一、寫在開頭
依稀記得多年以前的一場面試中,面試官從Java并發編程問到了鎖,從鎖問到了原子性,從原子性問到了Atomic類庫(對著JUC包進行了刨根問底),從Atomic問到了CAS算法,緊接著又有追問到了底層的Unsafe類,當問到Unsafe類時,我就知道這場面試廢了,這似乎把祖墳都能給問冒煙啊。
但時過境遷,現在再回想其那場面試,不再覺得面試官的追毛求疵,反而為那時候青澀菜雞的自己感到羞愧,為什么這樣說呢,實事求是的說Unsafe類雖然是比較底層,并且我們日常開發不可能用到的類,但是!翻開JUC包中的很多工具類,只要底層用到了CAS思想來提升并發性能的,幾乎都脫離不了Unsafe類的運用,可惜那時候光知道被八股文了,沒有做到細心總結與發現。
二、Unsafe的基本介紹
我們知道C語言可以通過指針去操作內存空間,Java不存在指針,為了提升Java運行效率、增強Java語言底層資源操作能力,便誕生了Unsafe類,Unsafe是位于sun.misc包下。正如它的名字一樣,這種操作底層的方式是不安全的,在程序中過度和不合理的使用,會帶來未知的風險,因此,Unsafe雖然,但要慎用哦!
2.1 如何創建一個unsafe實例
我們無法直接通過new的方式創建一個unsafe的實例,為什么呢?我們看它的這段源碼便知:
public final class Unsafe {// 單例對象private static final Unsafe theUnsafe;private Unsafe() {}@CallerSensitivepublic static Unsafe getUnsafe() {Class var0 = Reflection.getCallerClass();// 僅在啟動類加載器`BootstrapClassLoader`加載時才合法if(!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe");} else {return theUnsafe;}}
}
從源碼中我們發現Unsafe類被final修飾,所以無法被繼承,同時它的無參構造方法被private修飾,也無法通過new去直接實例化,不過在Unsafe 類提供了一個靜態方法getUnsafe,看上去貌似可以用它來獲取 Unsafe 實例。但是!當我們直接去調用這個方法的時候,會報如下錯誤:
Exception in thread "main" java.lang.SecurityException: Unsafeat sun.misc.Unsafe.getUnsafe(Unsafe.java:90)at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)
這是因為在getUnsafe方法中,會對調用者的classLoader進行檢查,判斷當前類是否由Bootstrap classLoader加載,如果不是的話就會拋出一個SecurityException異常。
那我們如果想使用Unsafe類,到底怎樣才能獲取它的實例呢?
在這里提供給大家兩種方式:
方式一
假若在A類中調用Unsafe實例,則可通過Java命令行命令-Xbootclasspath/a把調用Unsafe相關方法的類A所在jar包路徑追加到默認的bootstrap路徑中,使得A被啟動類加載器加載,從而通過Unsafe.getUnsafe方法安全的獲取Unsafe實例。
java -Xbootclasspath/a: ${path} // 其中path為調用Unsafe相關方法的類所在jar包路徑
方式二
利用反射獲得 Unsafe 類中已經實例化完成的單例對象:
public static Unsafe getUnsafe() throws IllegalAccessException {Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");//Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同unsafeField.setAccessible(true);Unsafe unsafe =(Unsafe) unsafeField.get(null);return unsafe;}
2.2 Unsafe的使用
上面我們已經知道了如何獲取一個unsafe實例了,那現在就開始寫一個小demo來感受一下它的使用吧。
public class TestService {//通過單例獲取實例public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");//Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同unsafeField.setAccessible(true);Unsafe unsafe =(Unsafe) unsafeField.get(null);return unsafe;}//調用實例方法去賦值public void fieldTest(Unsafe unsafe) throws NoSuchFieldException {Persion persion = new Persion();persion.setAge(10);System.out.println("ofigin_age:" + persion.getAge());long fieldOffset = unsafe.objectFieldOffset(Persion.class.getDeclaredField("age"));System.out.println("offset:"+fieldOffset);unsafe.putInt(persion,fieldOffset,20);System.out.println("new_age:"+unsafe.getInt(persion,fieldOffset));}public static void main(String[] args) {TestService testService = new TestService();try {testService.fieldTest(getUnsafe());} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();}}
}
class Persion{private String name;private int age;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}
}
輸出:
ofigin_age:10
offset:12
new_age:20
通過 Unsafe 類的objectFieldOffset方法獲取到了對象中字段的偏移地址,這個偏移地址不是內存中的絕對地址而是一個相對地址,之后再通過這個偏移地址對int類型字段的屬性值進行讀寫操作,通過結果也可以看到 Unsafe 的方法和類中的get方法獲取到的值是相同的。
三、Unsafe類的8種應用
基于Unsafe所提供的API,我們大致可以將Unsafe根據應用場景分為如下的八類,上一個腦圖。
3.1 內存操作
學習過C或者C++的同學對于內存操作應該很熟悉了,在Java里我們是無法直接對內存進行操作的,我們創建的對象幾乎都在堆內內存中存放,它的內存分配與管理都是JVM去實現,同時,在Java中還存在一個JVM管控之外的內存區域叫做“堆外內存”,Java中對堆外內存的操作,依賴于Unsafe提供的操作堆外內存的native方法啦。
內存操作的常用方法:
/*包含堆外內存的分配、拷貝、釋放、給定地址值操作*/
//分配內存, 相當于C++的malloc函數
public native long allocateMemory(long bytes);
//擴充內存
public native long reallocateMemory(long address, long bytes);
//釋放內存
public native void freeMemory(long address);
//在給定的內存塊中設置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//內存拷貝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//獲取給定地址值,忽略修飾限定符的訪問限制。與此類似操作還有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//為給定地址設置值,忽略修飾限定符的訪問限制,與此類似操作還有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//獲取給定地址的byte類型的值(當且僅當該內存地址為allocateMemory分配時,此方法結果為確定的)
public native byte getByte(long address);
//為給定地址設置byte類型的值(當且僅當該內存地址為allocateMemory分配時,此方法結果才是確定的)
public native void putByte(long address, byte x);
在這里我們不僅會想,為啥全是native方法呢?
- native方法通過JNI調用了其他語言,如果C++等提供的現車功能,可以讓Java拿來即用;
- 需要用到 Java 中不具備的依賴于操作系統的特性,Java 在實現跨平臺的同時要實現對底層的控制,需要借助其他語言發揮作用;
- 程序對時間敏感或對性能要求非常高時,有必要使用更加底層的語言,例如 C/C++甚至是匯編。
【經典應用】
在Netty、MINA等NIO框架中我們常常會應到緩沖池,而實現緩沖池的一個重要類就是DirectByteBuffer,它主要的作用對于堆外內存的創建、使用、銷毀等工作。
通常在I/O通信過程中,會存在堆內內存到堆外內存的數據拷貝操作,對于需要頻繁進行內存間數據拷貝且生命周期較短的暫存數據,都建議存儲到堆外內存
從上圖我們可以看到,在構建實例時,DirectByteBuffer內部通過Unsafe.allocateMemory分配內存、Unsafe.setMemory進行內存初始化,而后構建Cleaner對象用于跟蹤DirectByteBuffer對象的垃圾回收,以實現當DirectByteBuffer被垃圾回收時,分配的堆外內存一起被釋放。
3.2 內存屏障
為了充分利用緩存,提高程序的執行速度,編譯器在底層執行的時候,會進行指令重排序的優化操作,但這種優化,在有些時候會帶來 有序性 的問題。(在將volatile關鍵字的時候提到過了)
為了解決這一問題,Java中引入了內存屏障(Memory Barrier 又稱內存柵欄,是一個 CPU 指令),通過組織屏障兩邊的指令重排序從而避免編譯器和硬件的不正確優化情況。
在Unsafe類中提供了3個native方法來實現內存屏障:
//內存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//內存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//內存屏障,禁止load、store操作重排序
public native void fullFence();
【經典應用】
在之前的文章中,我們講過Java8中引入的一個高性能的讀寫鎖:StampedLock(鎖王),在這個鎖中同時支持悲觀讀與樂觀讀,悲觀讀就和ReentrantLock一致,樂觀讀中就使用到了unsafe的loadFence(),一起去看一下。
/*** 使用樂觀讀鎖訪問共享資源* 注意:樂觀讀鎖在保證數據一致性上需要拷貝一份要操作的變量到方法棧,并且在操作數據時候 可能其他寫線程已經修改了數據,* 而我們操作的是方法棧里面的數據,也就是一個快照,所以最多返回的不是最新的數據,但是一致性還是得到保障的。** @return*/double distanceFromOrigin() {long stamp = sl.tryOptimisticRead(); // 獲取樂觀讀鎖double currentX = x, currentY = y; // 拷貝共享資源到本地方法棧中if (!sl.validate(stamp)) { // //檢查樂觀讀鎖后是否有其他寫鎖發生,有則返回falsestamp = sl.readLock(); // 獲取一個悲觀讀鎖try {currentX = x;currentY = y;} finally {sl.unlockRead(stamp); // 釋放悲觀讀鎖}}return Math.sqrt(currentX * currentX + currentY * currentY);}
在官網給出的樂觀讀的使用案例中,我們看到if中做了一個根絕印章校驗寫鎖發生的操作,我們跟入這個校驗源碼中:
public boolean validate(long stamp) {U.loadFence();//load內存屏障return (stamp & SBITS) == (state & SBITS);}
這一步的目的是防止鎖狀態校驗運算發生重排序導致鎖狀態校驗不準確的問題!
3.3 對象操作
其實在2.2 Unsafe的使用中,我們已經使用了Unsafe進行對象成員屬性的內存偏移量獲取,以及字段屬性值的修改功能了,除了Int類型,Unsafe還支持對所有8種基本數據類型以及Object的內存數據修改,這里就不再贅述了。
需要額外強掉的一點,在Unsafe的源碼中還提供了一種非常規的方式進行對象的實例化:
//繞過構造方法、初始化代碼來創建對象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;
這種方法可以繞過構造方法和初始化代碼塊來創建對象,我們寫一個小demo學習一下。
@Datapublic class A {private int b;public A(){this.b =1;}}
定義一個類A,我們分別采用無參構造器、newInstance()、Unsafe方法進行實例化。
public void objTest() throws Exception{A a1=new A();System.out.println(a1.getB());A a2 = A.class.newInstance();System.out.println(a2.getB());A a3= (A) unsafe.allocateInstance(A.class);System.out.println(a3.getB());}
輸出結果為1,1,0。這說明調用unsafe的allocateInstance方法確實可以跳過構造器去實例化對象!
3.4 數組操作
在 Unsafe 中,可以使用arrayBaseOffset方法獲取數組中第一個元素的偏移地址,使用arrayIndexScale方法可以獲取數組中元素間的偏移地址增量,通過這兩個方法可以定位數組中的每個元素在內存中的位置。
基于2.2 Unsafe使用的測試代碼,我們增加如下的方法:
//獲取數組元素在內存中的偏移地址,以及偏移量private void arrayTest(Unsafe unsafe) {String[] array=new String[]{"aaa","bb","cc"};int baseOffset = unsafe.arrayBaseOffset(String[].class);System.out.println("數組第一個元素的偏移地址:" + baseOffset);int scale = unsafe.arrayIndexScale(String[].class);System.out.println("元素偏移量" + scale);for (int i = 0; i < array.length; i++) {int offset=baseOffset+scale*i;System.out.println(offset+" : "+unsafe.getObject(array,offset));}}
輸出:
數組第一個元素的偏移地址:16
元素偏移量4
16 : aaa
20 : bb
24 : cc
3.5 CAS相關
終于,重點來了,我們寫這篇文章的初衷是什么?是回想起曾經面時,面試官由原子類庫(Atomic)問到了CAS算法,從而追問到了Unsafe類上,在JUC包中到處都可以看到CAS的身影,在java.util.concurrent.atomic相關類、Java AQS、CurrentHashMap等等類中均有!
以AtomicInteger為例,在內部提供了一個方法為compareAndSet(int expect, int update) ,如果輸入的數值等于預期值,則以原子方式將該值設置為輸入值(update),而它的底層調用則是unsafe的compareAndSwapInt()方法。
public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}
CAS思想的底層實現其實就是Unsafe類中的幾個native本地方法:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
3.6 線程調度
Unsafe 類中提供了park、unpark、monitorEnter、monitorExit、tryMonitorEnter方法進行線程調度,在前面介紹 AQS 的文章中我們學過,在AQS中通過調用LockSupport.park()和LockSupport.unpark()實現線程的阻塞和喚醒的,而LockSupport的park、unpark方法實際是調用Unsafe的park、unpark方式來實現。
//取消阻塞線程
public native void unpark(Object thread);
//阻塞線程
public native void park(boolean isAbsolute, long time);
//獲得對象鎖(可重入鎖)
@Deprecated
public native void monitorEnter(Object o);
//釋放對象鎖
@Deprecated
public native void monitorExit(Object o);
//嘗試獲取對象鎖
@Deprecated
public native boolean tryMonitorEnter(Object o);
LockSupport源碼:
public static void park(Object blocker) {Thread t = Thread.currentThread();setBlocker(t, blocker);UNSAFE.park(false, 0L);setBlocker(t, null);}public static void unpark(Thread thread) {if (thread != null)UNSAFE.unpark(thread);}
3.7 Class操作
Unsafe 對Class的相關操作主要包括靜態字段內存定位、定義類、定義匿名類、檢驗&確保初始化等。
//獲取給定靜態字段的內存地址偏移量,這個值對于給定的字段是唯一且固定不變的
public native long staticFieldOffset(Field f);
//獲取一個靜態類中給定字段的對象指針
public native Object staticFieldBase(Field f);
//判斷是否需要初始化一個類,通常在獲取一個類的靜態屬性的時候(因為一個類如果沒初始化,它的靜態屬性也不會初始化)使用。 當且僅當ensureClassInitialized方法不生效時返回false。
public native boolean shouldBeInitialized(Class<?> c);
//檢測給定的類是否已經初始化。通常在獲取一個類的靜態屬性的時候(因為一個類如果沒初始化,它的靜態屬性也不會初始化)使用。
public native void ensureClassInitialized(Class<?> c);
//定義一個類,此方法會跳過JVM的所有安全檢查,默認情況下,ClassLoader(類加載器)和ProtectionDomain(保護域)實例來源于調用者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//定義一個匿名類
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
【測試案例】
@Datapublic class User {public static String name="javabuild";int age;}private void staticTest() throws Exception {User user=new User();//判斷是否需要初始化一個類,通常在獲取一個類的靜態屬性的時候(因為一個類如果沒初始化,它的靜態屬性也不會初始化)使用System.out.println(unsafe.shouldBeInitialized(User.class));Field sexField = User.class.getDeclaredField("name");//獲取給定靜態字段的內存地址偏移量long fieldOffset = unsafe.staticFieldOffset(sexField);//獲取一個靜態類中給定字段的對象指針Object fieldBase = unsafe.staticFieldBase(sexField);//根據某個字段對象指針和偏移量可以唯一定位這個字段。Object object = unsafe.getObject(fieldBase, fieldOffset);System.out.println(object);}
此外,在Java8中引入的Lambda表達式的實現中也使用到了defineClass和defineAnonymousClass方法。
3.8 系統信息
Unsafe 中提供的addressSize和pageSize方法用于獲取系統信息。
1) 調用addressSize方法會返回系統指針的大小,如果在 64 位系統下默認會返回 8,而 32 位系統則會返回 4。
2) 調用 pageSize 方法會返回內存頁的大小,值為 2 的整數冪。
使用下面的代碼可以直接進行打印:
private void systemTest() {System.out.println(unsafe.addressSize());System.out.println(unsafe.pageSize());
}
輸出為:8,4096
四、總結
哎呀,媽呀,終于寫完了,人要傻了,為了整理這篇文章看了大量的源碼,人看的頭大,跟俄羅斯套娃似的源碼,嚴謹的串聯在一起!Unsafe類在日常的面試中確實不經常被問到,大家稍微了解一下即可。
五、結尾彩蛋
如果本篇博客對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯系Build哥!
如果您想與Build哥的關系更近一步,還可以關注“JavaBuild888”,在這里除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!