Java雙刃劍之Unsafe類詳解
前一段時(shí)間在研究juc源碼的時(shí)候,發(fā)現(xiàn)在很多工具類中都調(diào)用了一個(gè)Unsafe類中的方法,出于好奇就想要研究一下這個(gè)類到底有什么作用,于是先查閱了一些資料,一查不要緊,很多資料中對(duì)Unsafe的態(tài)度都是這樣的畫風(fēng):

其實(shí)看到這些說法也沒什么意外,畢竟Unsafe這個(gè)詞直譯過來就是“不安全的”,從名字里我們也大概能看來Java的開發(fā)者們對(duì)它有些不放心。但是作為一名極客,不能你說不安全我就不去研究了,畢竟只有了解一項(xiàng)技術(shù)的風(fēng)險(xiǎn)點(diǎn),才能更好的避免出現(xiàn)這些問題嘛。
下面我們言歸正傳,先通過簡單的介紹來對(duì)Unsafe類有一個(gè)大致的了解。Unsafe類是一個(gè)位于sun.misc包下的類,它提供了一些相對(duì)底層方法,能夠讓我們接觸到一些更接近操作系統(tǒng)底層的資源,如系統(tǒng)的內(nèi)存資源、cpu指令等。而通過這些方法,我們能夠完成一些普通方法無法實(shí)現(xiàn)的功能,例如直接使用偏移地址操作對(duì)象、數(shù)組等等。但是在使用這些方法提供的便利的同時(shí),也存在一些潛在的安全因素,例如對(duì)內(nèi)存的錯(cuò)誤操作可能會(huì)引起內(nèi)存泄漏,嚴(yán)重時(shí)甚至可能引起jvm崩潰。因此在使用Unsafe前,我們必須要了解它的工作原理與各方法的應(yīng)用場(chǎng)景,并且在此基礎(chǔ)上仍需要非常謹(jǐn)慎的操作,下面我們正式開始對(duì)Unsafe的學(xué)習(xí)。
Unsafe 基礎(chǔ)
首先我們來嘗試獲取一個(gè)Unsafe實(shí)例,如果按照new的方式去創(chuàng)建對(duì)象,不好意思,編譯器會(huì)報(bào)錯(cuò)提示你:
Unsafe() has private access in 'sun.misc.Unsafe'
查看Unsafe類的源碼,可以看到它被final修飾不允許被繼承,并且構(gòu)造函數(shù)為private類型,即不允許我們手動(dòng)調(diào)用構(gòu)造方法進(jìn)行實(shí)例化,只有在static靜態(tài)代碼塊中,以單例的方式初始化了一個(gè)Unsafe對(duì)象:
public final class Unsafe {
private static final Unsafe theUnsafe;
...
private Unsafe() {
}
...
static {
theUnsafe = new Unsafe();
}
}
在Unsafe類中,提供了一個(gè)靜態(tài)方法getUnsafe,看上去貌似可以用它來獲取Unsafe實(shí)例:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
但是如果我們直接調(diào)用這個(gè)靜態(tài)方法,會(huì)拋出異常:
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)
這是因?yàn)樵?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;">getUnsafe方法中,會(huì)對(duì)調(diào)用者的classLoader進(jìn)行檢查,判斷當(dāng)前類是否由Bootstrap classLoader加載,如果不是的話那么就會(huì)拋出一個(gè)SecurityException異常。也就是說,只有啟動(dòng)類加載器加載的類才能夠調(diào)用Unsafe類中的方法,來防止這些方法在不可信的代碼中被調(diào)用。
那么,為什么要對(duì)Unsafe類進(jìn)行這么謹(jǐn)慎的使用限制呢,說到底,還是因?yàn)樗鼘?shí)現(xiàn)的功能過于底層,例如直接進(jìn)行內(nèi)存操作、繞過jvm的安全檢查創(chuàng)建對(duì)象等等,概括的來說,Unsafe類實(shí)現(xiàn)功能可以被分為下面8類:

創(chuàng)建實(shí)例
看到上面的這些功能,你是不是已經(jīng)有些迫不及待想要試一試了。那么如果我們執(zhí)意想要在自己的代碼中調(diào)用Unsafe類的方法,應(yīng)該怎么獲取一個(gè)它的實(shí)例對(duì)象呢,答案是利用反射獲得Unsafe類中已經(jīng)實(shí)例化完成的單例對(duì)象:
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;
}
在獲取到Unsafe的實(shí)例對(duì)象后,我們就可以使用它為所欲為了,先來嘗試使用它對(duì)一個(gè)對(duì)象的屬性進(jìn)行讀寫:
public void fieldTest(Unsafe unsafe) throws NoSuchFieldException {
User user=new User();
long fieldOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("age"));
System.out.println("offset:"+fieldOffset);
unsafe.putInt(user,fieldOffset,20);
System.out.println("age:"+unsafe.getInt(user,fieldOffset));
System.out.println("age:"+user.getAge());
}
運(yùn)行代碼輸出如下,可以看到通過Unsafe類的objectFieldOffset方法獲取了對(duì)象中字段的偏移地址,這個(gè)偏移地址不是內(nèi)存中的絕對(duì)地址而是一個(gè)相對(duì)地址,之后再通過這個(gè)偏移地址對(duì)int類型字段的屬性值進(jìn)行了讀寫操作,通過結(jié)果也可以看到Unsafe的方法和類中的get方法獲取到的值是相同的。
offset:12
age:20
age:20
在上面的例子中調(diào)用了Unsafe類的putInt和getInt方法,看一下源碼中的方法:
public native int getInt(Object o, long offset);
public native void putInt(Object o, long offset, int x);
先說作用,getInt用于從對(duì)象的指定偏移地址處讀取一個(gè)int,putInt用于在對(duì)象指定偏移地址處寫入一個(gè)int,并且即使類中的這個(gè)屬性是private私有類型的,也可以對(duì)它進(jìn)行讀寫。但是有細(xì)心的小伙伴可能發(fā)現(xiàn)了,這兩個(gè)方法相對(duì)于我們平常寫的普通方法,多了一個(gè)native關(guān)鍵字修飾,并且沒有具體的方法邏輯,那么它是怎么實(shí)現(xiàn)的呢?
native方法
在java中,這類方法被稱為native方法(Native Method),簡單的說就是由java調(diào)用非java代碼的接口,被調(diào)用的方法是由非java 語言實(shí)現(xiàn)的,例如它可以由C或C++語言來實(shí)現(xiàn),并編譯成DLL,然后直接供java進(jìn)行調(diào)用。native方法是通過JNI(Java Native Interface)實(shí)現(xiàn)調(diào)用的,從 java1.1開始 JNI 標(biāo)準(zhǔn)就是java平臺(tái)的一部分,它允許java代碼和其他語言的代碼進(jìn)行交互。

Unsafe類中的很多基礎(chǔ)方法都屬于native方法,那么為什么要使用native方法呢?原因可以概括為以下幾點(diǎn):
需要用到 java 中不具備的依賴于操作系統(tǒng)的特性,java在實(shí)現(xiàn)跨平臺(tái)的同時(shí)要實(shí)現(xiàn)對(duì)底層的控制,需要借助其他語言發(fā)揮作用 對(duì)于其他語言已經(jīng)完成的一些現(xiàn)成功能,可以使用java直接調(diào)用 程序?qū)r(shí)間敏感或?qū)π阅芤蠓浅8邥r(shí),有必要使用更加底層的語言,例如C/C++甚至是匯編
在juc包的很多并發(fā)工具類在實(shí)現(xiàn)并發(fā)機(jī)制時(shí),都調(diào)用了native方法,通過它們打破了java運(yùn)行時(shí)的界限,能夠接觸到操作系統(tǒng)底層的某些功能。對(duì)于同一個(gè)native方法,不同的操作系統(tǒng)可能會(huì)通過不同的方式來實(shí)現(xiàn),但是對(duì)于使用者來說是透明的,最終都會(huì)得到相同的結(jié)果,至于java如何實(shí)現(xiàn)的通過JNI調(diào)用其他語言的代碼,不是本文的重點(diǎn),會(huì)在后續(xù)的文章中具體學(xué)習(xí)。
Unsafe 應(yīng)用
在對(duì)Unsafe的基礎(chǔ)有了一定了解后,我們來看一下它的基本應(yīng)用。由于篇幅有限,不能對(duì)所有方法進(jìn)行介紹,如果大家有學(xué)習(xí)的需要,可以下載openJDK的源碼進(jìn)行學(xué)習(xí)。
1、內(nèi)存操作
如果你是一個(gè)寫過c或者c++的程序員,一定對(duì)內(nèi)存操作不會(huì)陌生,而在java中是不允許直接對(duì)內(nèi)存進(jìn)行操作的,對(duì)象內(nèi)存的分配和回收都是由jvm自己實(shí)現(xiàn)的。但是在Unsafe中,提供的下列接口可以直接進(jìn)行內(nèi)存操作:
//分配新的本地空間
public native long allocateMemory(long bytes);
//重新調(diào)整內(nèi)存空間的大小
public native long reallocateMemory(long address, long bytes);
//將內(nèi)存設(shè)置為指定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//內(nèi)存拷貝
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//清除內(nèi)存
public native void freeMemory(long address);
使用下面的代碼進(jìn)行測(cè)試:
private void memoryTest() {
int size = 4;
long addr = unsafe.allocateMemory(size);
long addr3 = unsafe.reallocateMemory(addr, size * 2);
System.out.println("addr: "+addr);
System.out.println("addr3: "+addr3);
try {
unsafe.setMemory(null,addr ,size,(byte)1);
for (int i = 0; i < 2; i++) {
unsafe.copyMemory(null,addr,null,addr3+size*i,4);
}
System.out.println(unsafe.getInt(addr));
System.out.println(unsafe.getLong(addr3));
}finally {
unsafe.freeMemory(addr);
unsafe.freeMemory(addr3);
}
}
先看結(jié)果輸出:
addr: 2433733895744
addr3: 2433733894944
16843009
72340172838076673
分析一下運(yùn)行結(jié)果,首先使用allocateMemory方法申請(qǐng)4字節(jié)長度的內(nèi)存空間,在循環(huán)中調(diào)用setMemory方法向每個(gè)字節(jié)寫入內(nèi)容為byte類型的1,當(dāng)使用Unsafe調(diào)用getInt方法時(shí),因?yàn)橐粋€(gè)int型變量占4個(gè)字節(jié),會(huì)一次性讀取4個(gè)字節(jié),組成一個(gè)int的值,對(duì)應(yīng)的十進(jìn)制結(jié)果為16843009,可以通過圖示理解這個(gè)過程:

在代碼中調(diào)用reallocateMemory方法重新分配了一塊8字節(jié)長度的內(nèi)存空間,通過比較addr和addr3可以看到和之前申請(qǐng)的內(nèi)存地址是不同的。在代碼中的第二個(gè)for循環(huán)里,調(diào)用copyMemory方法進(jìn)行了兩次內(nèi)存的拷貝,每次拷貝內(nèi)存地址addr開始的4個(gè)字節(jié),分別拷貝到以addr3和addr3+4開始的內(nèi)存空間上:

拷貝完成后,使用getLong方法一次性讀取8個(gè)字節(jié),得到long類型的值為72340172838076673。
需要注意,通過這種方式分配的內(nèi)存屬于堆外內(nèi)存,是無法進(jìn)行垃圾回收的,需要我們把這些內(nèi)存當(dāng)做一種資源去手動(dòng)調(diào)用freeMemory方法進(jìn)行釋放,否則會(huì)產(chǎn)生內(nèi)存泄漏。通用的操作內(nèi)存方式是在try中執(zhí)行對(duì)內(nèi)存的操作,最終在finally塊中進(jìn)行內(nèi)存的釋放。
2、內(nèi)存屏障
在介紹內(nèi)存屏障前,需要知道編譯器和CPU會(huì)在保證程序輸出結(jié)果一致的情況下,會(huì)對(duì)代碼進(jìn)行重排序,從指令優(yōu)化角度提升性能。而指令重排序可能會(huì)帶來一個(gè)不好的結(jié)果,導(dǎo)致CPU的高速緩存和內(nèi)存中數(shù)據(jù)的不一致,而內(nèi)存屏障(Memory Barrier)就是通過組織屏障兩邊的指令重排序從而避免編譯器和硬件的不正確優(yōu)化情況。
在硬件層面上,內(nèi)存屏障是CPU為了防止代碼進(jìn)行重排序而提供的指令,不同的硬件平臺(tái)上實(shí)現(xiàn)內(nèi)存屏障的方法可能并不相同。在java8中,引入了3個(gè)內(nèi)存屏障的函數(shù),它屏蔽了操作系統(tǒng)底層的差異,允許在代碼中定義、并統(tǒng)一由jvm來生成內(nèi)存屏障指令,來實(shí)現(xiàn)內(nèi)存屏障的功能。Unsafe中提供了下面三個(gè)內(nèi)存屏障相關(guān)方法:
//禁止讀操作重排序
public native void loadFence();
//禁止寫操作重排序
public native void storeFence();
//禁止讀、寫操作重排序
public native void fullFence();
內(nèi)存屏障可以看做對(duì)內(nèi)存隨機(jī)訪問的操作中的一個(gè)同步點(diǎn),使得此點(diǎn)之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點(diǎn)之后的操作。以loadFence方法為例,它會(huì)禁止讀操作重排序,保證在這個(gè)屏障之前的所有讀操作都已經(jīng)完成,并且將緩存數(shù)據(jù)設(shè)為無效,重新從主存中進(jìn)行加載。
看到這估計(jì)很多小伙伴們會(huì)想到volatile關(guān)鍵字了,如果在字段上添加了volatile關(guān)鍵字,就能夠?qū)崿F(xiàn)字段在多線程下的可見性。基于讀內(nèi)存屏障,我們也能實(shí)現(xiàn)相同的功能。下面定義一個(gè)線程方法,在線程中去修改flag標(biāo)志位,注意這里的flag是沒有被volatile修飾的:
@Getter
class ChangeThread implements Runnable{
/**volatile**/ boolean flag=false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread change flag to:" + flag);
flag = true;
}
}
在主線程的while循環(huán)中,加入內(nèi)存屏障,測(cè)試是否能夠感知到flag的修改變化:
public static void main(String[] args){
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
boolean flag = changeThread.isFlag();
unsafe.loadFence(); //加入讀內(nèi)存屏障
if (flag){
System.out.println("detected flag changed");
break;
}
}
System.out.println("main thread end");
}
運(yùn)行結(jié)果:
subThread change flag to:false
detected flag changed
main thread end
而如果刪掉上面代碼中的loadFence方法,那么主線程將無法感知到flag發(fā)生的變化,會(huì)一直在while中循環(huán)。可以用圖來表示上面的過程:

了解java內(nèi)存模型(JMM)的小伙伴們應(yīng)該清楚,運(yùn)行中的線程不是直接讀取主內(nèi)存中的變量的,只能操作自己工作內(nèi)存中的變量,然后同步到主內(nèi)存中,并且線程的工作內(nèi)存是不能共享的。上面的圖中的流程就是子線程借助于主內(nèi)存,將修改后的結(jié)果同步給了主線程,進(jìn)而修改主線程中的工作空間,跳出循環(huán)。
3、對(duì)象操作
a、對(duì)象成員屬性的內(nèi)存偏移量獲取,以及字段屬性值的修改,在上面的例子中我們已經(jīng)測(cè)試過了。除了前面的putInt、getInt方法外,Unsafe提供了全部8種基礎(chǔ)數(shù)據(jù)類型以及Object的put和get方法,并且所有的put方法都可以越過訪問權(quán)限,直接修改內(nèi)存中的數(shù)據(jù)。閱讀openJDK源碼中的注釋發(fā)現(xiàn),基礎(chǔ)數(shù)據(jù)類型和Object的讀寫稍有不同,基礎(chǔ)數(shù)據(jù)類型是直接操作的屬性值(value),而Object的操作則是基于引用值(reference value)。下面是Object的讀寫方法:
//在對(duì)象的指定偏移地址獲取一個(gè)對(duì)象引用
public native Object getObject(Object o, long offset);
//在對(duì)象指定偏移地址寫入一個(gè)對(duì)象引用
public native void putObject(Object o, long offset, Object x);
除了對(duì)象屬性的普通讀寫外,Unsafe還提供了volatile讀寫和有序?qū)懭?/strong>方法。volatile讀寫方法的覆蓋范圍與普通讀寫相同,包含了全部基礎(chǔ)數(shù)據(jù)類型和Object類型,以int類型為例:
//在對(duì)象的指定偏移地址處讀取一個(gè)int值,支持volatile load語義
public native int getIntVolatile(Object o, long offset);
//在對(duì)象指定偏移地址處寫入一個(gè)int,支持volatile store語義
public native void putIntVolatile(Object o, long offset, int x);
相對(duì)于普通讀寫來說,volatile讀寫具有更高的成本,因?yàn)樗枰WC可見性和有序性。在執(zhí)行get操作時(shí),會(huì)強(qiáng)制從主存中獲取屬性值,在使用put方法設(shè)置屬性值時(shí),會(huì)強(qiáng)制將值更新到主存中,從而保證這些變更對(duì)其他線程是可見的。
有序?qū)懭氲姆椒ㄓ幸韵氯齻€(gè):
public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);
有序?qū)懭氲某杀鞠鄬?duì)volatile較低,因?yàn)樗槐WC寫入時(shí)的有序性,而不保證可見性,也就是一個(gè)線程寫入的值不能保證其他線程立即可見。為了解決這里的差異性,需要對(duì)內(nèi)存屏障的知識(shí)點(diǎn)再進(jìn)一步進(jìn)行補(bǔ)充,首先需要了解兩個(gè)指令的概念:
Load:將主內(nèi)存中的數(shù)據(jù)拷貝到處理器的緩存中Store:將處理器緩存的數(shù)據(jù)刷新到主內(nèi)存中
順序?qū)懭肱cvolatile寫入的差別在于,在順序?qū)憰r(shí)加入的內(nèi)存屏障類型為StoreStore類型,而在volatile寫入時(shí)加入的內(nèi)存屏障是StoreLoad類型,如下圖所示:

在有序?qū)懭敕椒ㄖ校褂玫氖?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;">StoreStore屏障,該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存,這一操作先于Store2以及后續(xù)的存儲(chǔ)指令操作。而在volatile寫入中,使用的是StoreLoad屏障,該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存,這一操作先于Load2及后續(xù)的裝載指令,并且,StoreLoad屏障會(huì)使該屏障之前的所有內(nèi)存訪問指令,包括存儲(chǔ)指令和訪問指令全部完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問指令。
綜上所述,在上面的三類寫入方法中,在寫入效率方面,按照put、putOrder、putVolatile的順序效率逐漸降低,
b、使用Unsafe的allocateInstance方法,允許我們使用非常規(guī)的方式進(jìn)行對(duì)象的實(shí)例化,首先定義一個(gè)實(shí)體類,并且在構(gòu)造函數(shù)中對(duì)其成員變量進(jìn)行賦值操作:
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}
分別基于構(gòu)造函數(shù)、反射以及Unsafe方法的不同方式創(chuàng)建對(duì)象進(jìn)行比較:
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());
}
打印結(jié)果分別為1、1、0,說明通過allocateInstance方法創(chuàng)建對(duì)象過程中,不會(huì)調(diào)用類的構(gòu)造方法。使用這種方式創(chuàng)建對(duì)象時(shí),只用到了Class對(duì)象,所以說如果想要跳過對(duì)象的初始化階段或者跳過構(gòu)造器的安全檢查,就可以使用這種方法。在上面的例子中,如果將A類的構(gòu)造函數(shù)改為private類型,將無法通過構(gòu)造函數(shù)和反射創(chuàng)建對(duì)象,但allocateInstance方法仍然有效。
4、數(shù)組操作
在Unsafe中,可以使用arrayBaseOffset方法可以獲取數(shù)組中第一個(gè)元素的偏移地址,使用arrayIndexScale方法可以獲取數(shù)組中元素間的偏移地址增量。使用下面的代碼進(jìn)行測(cè)試:
private void arrayTest() {
String[] array=new String[]{"str1str1str","str2","str3"};
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));
}
}
上面代碼的輸出結(jié)果為:
16
4
16 : str1str1str
20 : str2
24 : str3
通過配合使用數(shù)組偏移首地址和各元素間偏移地址的增量,可以方便的定位到數(shù)組中的元素在內(nèi)存中的位置,進(jìn)而通過getObject方法直接獲取任意位置的數(shù)組元素。需要說明的是,arrayIndexScale獲取的并不是數(shù)組中元素占用的大小,而是地址的增量,按照openJDK中的注釋,可以將它翻譯為元素尋址的轉(zhuǎn)換因子(scale factor for addressing elements)。在上面的例子中,第一個(gè)字符串長度為11字節(jié),但其地址增量仍然為4字節(jié)。
那么,基于這兩個(gè)值是如何實(shí)現(xiàn)的尋址和數(shù)組元素的訪問呢,這里需要借助一點(diǎn)在前面的文章中講過的Java對(duì)象內(nèi)存布局的知識(shí),先把上面例子中的String數(shù)組對(duì)象的內(nèi)存布局畫出來,就很方便大家理解了:

在String數(shù)組對(duì)象中,對(duì)象頭包含3部分,mark word標(biāo)記字占用8字節(jié),klass point類型指針占用4字節(jié),數(shù)組對(duì)象特有的數(shù)組長度部分占用4字節(jié),總共占用了16字節(jié)。第一個(gè)String的引用類型相對(duì)于對(duì)象的首地址的偏移量是就16,之后每個(gè)元素在這個(gè)基礎(chǔ)上加4,正好對(duì)應(yīng)了我們上面代碼中的尋址過程,之后再使用前面說過的getObject方法,通過數(shù)組對(duì)象可以獲得對(duì)象在堆中的首地址,再配合對(duì)象中變量的偏移量,就能獲得每一個(gè)變量的引用。
5、CAS操作
在juc包的并發(fā)工具類中大量地使用了CAS操作,像在前面介紹synchronized和AQS的文章中也多次提到了CAS,其作為樂觀鎖在并發(fā)工具類中廣泛發(fā)揮了作用。在Unsafe類中,提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong方法來實(shí)現(xiàn)的對(duì)Object、int、long類型的CAS操作。以compareAndSwapInt方法為例:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
參數(shù)中o為需要更新的對(duì)象,offset是對(duì)象o中整形字段的偏移量,如果這個(gè)字段的值與expected相同,則將字段的值設(shè)為x這個(gè)新值,并且此更新是不可被中斷的,也就是一個(gè)原子操作。下面是一個(gè)使用compareAndSwapInt的例子:
private volatile int a;
public static void main(String[] args){
CasTest casTest=new CasTest();
new Thread(()->{
for (int i = 1; i < 5; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
new Thread(()->{
for (int i = 5 ; i <10 ; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
}
private void increment(int x){
while (true){
try {
long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
break;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
運(yùn)行代碼會(huì)依次輸出:
1 2 3 4 5 6 7 8 9
在上面的例子中,使用兩個(gè)線程去修改int型屬性a的值,并且只有在a的值等于傳入的參數(shù)x減一時(shí),才會(huì)將a的值變?yōu)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;">x,也就是實(shí)現(xiàn)對(duì)a的加一的操作。流程如下所示:

需要注意的是,在調(diào)用compareAndSwapInt方法后,會(huì)直接返回true或false的修改結(jié)果,因此需要我們?cè)诖a中手動(dòng)添加自旋的邏輯。在AtomicInteger類的設(shè)計(jì)中,也是采用了將compareAndSwapInt的結(jié)果作為循環(huán)條件,直至修改成功才退出死循環(huán)的方式來實(shí)現(xiàn)的原子性的自增操作。
6、線程調(diào)度
Unsafe類中提供了park、unpark、monitorEnter、monitorExit、tryMonitorEnter方法進(jìn)行線程調(diào)度,在前面介紹AQS的文章中我們提到過使用LockSupport掛起或喚醒指定線程,看一下LockSupport的源碼,可以看到它也是調(diào)用的Unsafe類中的方法:
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);
}
LockSupport的park方法調(diào)用了Unsafe的park方法來阻塞當(dāng)前線程,此方法將線程阻塞后就不會(huì)繼續(xù)往后執(zhí)行,直到有其他線程調(diào)用unpark方法喚醒當(dāng)前線程。下面的例子對(duì)Unsafe的這兩個(gè)方法進(jìn)行測(cè)試:
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("subThread try to unpark mainThread");
unsafe.unpark(mainThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("park main mainThread");
unsafe.park(false,0L);
System.out.println("unpark mainThread success");
}
程序輸出為:
park main mainThread
subThread try to unpark mainThread
unpark mainThread success
程序運(yùn)行的流程也比較容易看懂,子線程開始運(yùn)行后先進(jìn)行睡眠,確保主線程能夠調(diào)用park方法阻塞自己,子線程在睡眠5秒后,調(diào)用unpark方法喚醒主線程,使主線程能繼續(xù)向下執(zhí)行。整個(gè)流程如下圖所示:

此外,Unsafe源碼中monitor相關(guān)的三個(gè)方法已經(jīng)被標(biāo)記為deprecated,不建議被使用:
//獲得對(duì)象鎖
@Deprecated
public native void monitorEnter(Object var1);
//釋放對(duì)象鎖
@Deprecated
public native void monitorExit(Object var1);
//嘗試獲得對(duì)象鎖
@Deprecated
public native boolean tryMonitorEnter(Object var1);
monitorEnter方法用于獲得對(duì)象鎖,monitorExit用于釋放對(duì)象鎖,如果對(duì)一個(gè)沒有被monitorEnter加鎖的對(duì)象執(zhí)行此方法,會(huì)拋出IllegalMonitorStateException異常。tryMonitorEnter方法嘗試獲取對(duì)象鎖,如果成功則返回true,反之返回false。
7、Class操作
Unsafe對(duì)Class的相關(guān)操作主要包括類加載和靜態(tài)變量的操作方法。
a、靜態(tài)屬性讀取相關(guān)的方法:
//獲取靜態(tài)屬性的偏移量
public native long staticFieldOffset(Field f);
//獲取靜態(tài)屬性的對(duì)象指針
public native Object staticFieldBase(Field f);
//判斷類是否需要實(shí)例化(用于獲取類的靜態(tài)屬性前進(jìn)行檢測(cè))
public native boolean shouldBeInitialized(Class<?> c);
創(chuàng)建一個(gè)包含靜態(tài)屬性的類,進(jìn)行測(cè)試:
@Data
public class User {
public static String name="Hydra";
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);
}
運(yùn)行結(jié)果:
false
Hydra
在Unsafe的對(duì)象操作中,我們學(xué)習(xí)了通過objectFieldOffset方法獲取對(duì)象屬性偏移量并基于它對(duì)變量的值進(jìn)行存取,但是它不適用于類中的靜態(tài)屬性,這時(shí)候就需要使用staticFieldOffset方法。在上面的代碼中,只有在獲取Field對(duì)象的過程中依賴到了Class,而獲取靜態(tài)變量的屬性時(shí)不再依賴于Class。
在上面的代碼中首先創(chuàng)建一個(gè)User對(duì)象,這是因?yàn)槿绻粋€(gè)類沒有被實(shí)例化,那么它的靜態(tài)屬性也不會(huì)被初始化,最后獲取的字段屬性將是null。所以在獲取靜態(tài)屬性前,需要調(diào)用shouldBeInitialized方法,判斷在獲取前是否需要初始化這個(gè)類。如果刪除創(chuàng)建User對(duì)象的語句,運(yùn)行結(jié)果會(huì)變?yōu)椋?/p>
true
null
b、使用defineClass方法允許程序在運(yùn)行時(shí)動(dòng)態(tài)地創(chuàng)建一個(gè)類,方法定義如下:
public native Class<?> defineClass(String name, byte[] b, int off, int len,
ClassLoader loader,ProtectionDomain protectionDomain);
在實(shí)際使用過程中,可以只傳入字節(jié)數(shù)組、起始字節(jié)的下標(biāo)以及讀取的字節(jié)長度,默認(rèn)情況下,類加載器(ClassLoader)和保護(hù)域(ProtectionDomain)來源于調(diào)用此方法的實(shí)例。下面的例子中實(shí)現(xiàn)了反編譯生成后的class文件的功能:
private static void defineTest() {
String fileName="F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class";
File file = new File(fileName);
try(FileInputStream fis = new FileInputStream(file)) {
byte[] content=new byte[(int)file.length()];
fis.read(content);
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object age = clazz.getMethod("getAge").invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
}
在上面的代碼中,首先讀取了一個(gè)class文件并通過文件流將它轉(zhuǎn)化為字節(jié)數(shù)組,之后使用defineClass方法動(dòng)態(tài)的創(chuàng)建了一個(gè)類,并在后續(xù)完成了它的實(shí)例化工作,流程如下圖所示,并且通過這種方式創(chuàng)建的類,會(huì)跳過JVM的所有安全檢查。

除了defineClass方法外,Unsafe還提供了一個(gè)defineAnonymousClass方法:
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
使用該方法可以用來動(dòng)態(tài)的創(chuàng)建一個(gè)匿名類,在Lambda表達(dá)式中就是使用ASM動(dòng)態(tài)生成字節(jié)碼,然后利用該方法定義實(shí)現(xiàn)相應(yīng)的函數(shù)式接口的匿名類。在jdk15發(fā)布的新特性中,在隱藏類(Hidden classes)一條中,指出將在未來的版本中棄用Unsafe的defineAnonymousClass方法。
8、系統(tǒng)信息
Unsafe中提供的addressSize和pageSize方法用于獲取系統(tǒng)信息,調(diào)用addressSize方法會(huì)返回系統(tǒng)指針的大小,如果在64位系統(tǒng)下默認(rèn)會(huì)返回8,而32位系統(tǒng)則會(huì)返回4。調(diào)用pageSize方法會(huì)返回內(nèi)存頁的大小,值為2的整數(shù)冪。使用下面的代碼可以直接進(jìn)行打印:
private void systemTest() {
System.out.println(unsafe.addressSize());
System.out.println(unsafe.pageSize());
}
執(zhí)行結(jié)果:
8
4096
這兩個(gè)方法的應(yīng)用場(chǎng)景比較少,在java.nio.Bits類中,在使用pageCount計(jì)算所需的內(nèi)存頁的數(shù)量時(shí),調(diào)用了pageSize方法獲取內(nèi)存頁的大小。另外,在使用copySwapMemory方法拷貝內(nèi)存時(shí),調(diào)用了addressSize方法,檢測(cè)32位系統(tǒng)的情況。
總結(jié)
在本文中,我們首先介紹了Unsafe的基本概念、工作原理,并在此基礎(chǔ)上,對(duì)它的API進(jìn)行了說明與實(shí)踐。相信大家通過這一過程,能夠發(fā)現(xiàn)Unsafe在某些場(chǎng)景下,確實(shí)能夠?yàn)槲覀兲峁┚幊讨械谋憷5腔氐介_頭的話題,在使用這些便利時(shí),確實(shí)存在著一些安全上的隱患,在我看來,一項(xiàng)技術(shù)具有不安全因素并不可怕,可怕的是它在使用過程中被濫用。盡管之前有傳言說會(huì)在java9中移除Unsafe類,不過它還是照樣已經(jīng)存活到了jdk16,按照存在即合理的邏輯,只要使用得當(dāng),它還是能給我們帶來不少的幫助,因此最后還是建議大家,在使用Unsafe的過程中一定要做到使用謹(jǐn)慎使用、避免濫用。
我已經(jīng)更新了我的《10萬字Springboot經(jīng)典學(xué)習(xí)筆記》中,點(diǎn)擊下面小卡片,進(jìn)入【Java開發(fā)寶典】,回復(fù):筆記,即可免費(fèi)獲取。
點(diǎn)贊是最大的支持

