0.Unsafe介紹
JavaDoc說(shuō), Unsafe提供了一組用于執(zhí)行底層的,不安全操作的方法缔莲。那么具體有哪些方法呢,我畫(huà)了一張圖痴奏。
可以看到Unsafe中提供了CAS,內(nèi)存操作读拆,線(xiàn)程調(diào)度,本機(jī)信息檐晕,Class相關(guān)方法,查看和設(shè)置某個(gè)對(duì)象或字段辟灰,內(nèi)存分配和釋放相關(guān)操作,內(nèi)存地址獲取相關(guān)方法芥喇。我自己抽空對(duì)上述方法進(jìn)行了注釋?zhuān)?br> 你可以在這里看到。
那么如何使用Unsafe呢继控?下面我們就來(lái)說(shuō)說(shuō)如何獲取Unsafe并操作。
1.獲取Unsafe實(shí)例
如下所述霹崎,由于Unsafe.getUnsafe會(huì)判斷調(diào)用類(lèi)的類(lèi)加載器是否為引導(dǎo)類(lèi)加載器,如果是仿畸,可以正常獲取Unsafe實(shí)例,否則會(huì)拋出安全異常错沽。
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
主要有兩種方式來(lái)繞過(guò)安全檢查眶拉,一種是通過(guò)將使用Unsafe的類(lèi)交給bootstrap class loader去加載,另一種方式是通過(guò)反射忆植。
1.1 通過(guò)bootstrap class loader去加載Unsafe。
public class GetUnsafeFromMethod {
public static void main(String[] args){
//調(diào)用這個(gè)方法朝刊,必須要在啟動(dòng)類(lèi)加載器中獲取,否則會(huì)拋出安全異常
Unsafe unsafe = Unsafe.getUnsafe();
System.out.printf("addressSize=%d, pageSize=%d\n", unsafe.addressSize(), unsafe.pageSize());
}
}
上面代碼拾氓,直接執(zhí)行會(huì)報(bào)安全異常SecurityException,原因是當(dāng)前caller的類(lèi)加載器是應(yīng)用類(lèi)加載器(Application Class loader)咙鞍,而要求的是啟動(dòng)類(lèi)加載器趾徽,
因而!VM.isSystemDomainLoader(var0.getClassLoader())
返回false翰守,拋出異常。
但是通過(guò)下面的命令行蜡峰,我們把GetUnsafeFromMethod.java
追加到bootclasspath(啟動(dòng)類(lèi)加載路徑)上,就可以正常執(zhí)行了事示。
javac -source 1.8 -encoding UTF8 -bootclasspath "%JAVA_HOME%/jre/lib/rt.jar;." GetUnsafeFromMethod.java
java -Xbootclasspath:"%JAVA_HOME%/jre/lib/rt.jar;." GetUnsafeFromMethod
你也看到了這樣做有點(diǎn)費(fèi)事了,難不成每次啟動(dòng)都要加這么一大串指令,所以下面我們就來(lái)反射是不是好用些臀脏。
1.2 通過(guò)反射獲取Unsafe
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class GetUnsafeFromReflect {
public static void main(String[] args){
Unsafe unsafe = getUnsafe();
System.out.printf("addressSize=%d, pageSize=%d\n", unsafe.addressSize(), unsafe.pageSize());
}
public static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return unsafe;
}
}
嗯,通過(guò)反射就可以直接用了揉稚,是不是比上一種利用啟動(dòng)類(lèi)加載器加載的方式好用很多
3.Unsafe API 的使用
具體如何使用,可以查看這篇文章搀玖。
實(shí)際的應(yīng)用案例,可以查看美團(tuán)的一篇文章灌诅。
4. Unsafe 中CAS部分的實(shí)現(xiàn)
我們可以看到Unsafe中基本都是調(diào)用native方法,如果你比較好奇這個(gè)native方法又是如何實(shí)現(xiàn)的猜拾,那么就需要去JVM里面找對(duì)應(yīng)的實(shí)現(xiàn)。
到http://hg.openjdk.java.net/
進(jìn)行一步步選擇下載對(duì)應(yīng)的hotspot版本顽聂,我這里下載的是http://hg.openjdk.java.net/jdk8u/jdk8u60/hotspot/archive/tip.tar.gz
,
然后解hotspot目錄紊搪,發(fā)現(xiàn) \src\share\vm\prims\unsafe.cpp
,這個(gè)就是對(duì)應(yīng)jvm相關(guān)的c++實(shí)現(xiàn)類(lèi)了耀石。
比如我們對(duì)CAS部分的實(shí)現(xiàn)很感興趣,就可以在該文件中搜索compareAndSwapInt娶牌,此時(shí)可以看到對(duì)應(yīng)的JNI方法為Unsafe_CompareAndSwapInt
// These are the methods prior to the JSR 166 changes in 1.6.0
static JNINativeMethod methods_15[] = {
...
{CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z", FN_PTR(Unsafe_CompareAndSwapObject)},
{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)}
...
};
接著我們?cè)谒阉?code>Unsafe_CompareAndSwapInt的實(shí)現(xiàn),
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj); //查找要指定的對(duì)象
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); //獲取要操作的是對(duì)象的字段的內(nèi)存地址诗良。
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //執(zhí)行Atomic類(lèi)中的cmpxchg。
UNSAFE_END
可以看到最后會(huì)調(diào)用到Atomic::cmpxchg
里面的函數(shù)鉴裹,這個(gè)根據(jù)不同操作系統(tǒng)和不同CPU會(huì)有不同的實(shí)現(xiàn),但都放在hotspot\src\os_cpu
目錄下径荔,比如linux_64x
的,對(duì)應(yīng)類(lèi)就是hotspot\src\os_cpu\linux_x86\vm\atomic_linux_x86.inline.hpp
总处, 而windows_64x
的,對(duì)應(yīng)類(lèi)就是hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp
鹦马。(此處也說(shuō)明了為什么Java可以Write once, Run everywhere, 原因就是JVM源碼對(duì)不同操作系統(tǒng)和不同CPU有不同的實(shí)現(xiàn))
這里我們以linux_64x
的為例,查看Atomic::cmpxchg的實(shí)現(xiàn)
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
0.os::is_MP
os::is_MP()
在hotspot\src\share\vm\runtime\os.hpp
中菱肖,如下:
// Interface for detecting multiprocessor system
static inline bool is_MP() {
// During bootstrap if _processor_count is not yet initialized
// we claim to be MP as that is safest. If any platform has a
// stub generator that might be triggered in this phase and for
// which being declared MP when in fact not, is a problem - then
// the bootstrap routine for the stub generator needs to check
// the processor count directly and leave the bootstrap routine
// in place until called after initialization has ocurred.
return (_processor_count != 1) || AssumeMP;
}
1.__asm__:
表示接下來(lái)是內(nèi)聯(lián)的匯編代碼,這里使用asm語(yǔ)句可以將匯編指令直接包含在C代碼中稳强,主要是為了極致的性能。
2.volatile: 表示去掉優(yōu)化
3.LOCK_IF_MP 是一個(gè)宏定義退疫,即:#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
替換文本里面也是匯編代碼摹闽,LOCK_IF_MP根據(jù)當(dāng)前操作系統(tǒng)是否為多核處理器,來(lái)決定是否為cmpxchg指令添加lock前綴付鹿。如果有l(wèi)ock前綴的話(huà),則會(huì)根據(jù)CPU不同會(huì)采用鎖總線(xiàn)或者鎖cache line的方式舵匾,來(lái)實(shí)現(xiàn)緩存一致性。
4.cmpxchgl部分的解釋
"cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory"
cmpxchgl的詳細(xì)執(zhí)行過(guò)程:
首先坐梯,輸入是"r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp),表示compare_value存入eax寄存器,而exchange_value谎替、dest、mp的值存入任意的通用寄存器钱贯。嵌入式匯編規(guī)定把輸出和輸入寄存器按統(tǒng)一順序編號(hào),順序是從輸出寄存器序列從左到右從上到下以“%0”開(kāi)始秩命,分別記為%0、%1···%9弃锐。也就是說(shuō),輸出的eax是%0霹菊,輸入的exchange_value、compare_value旋廷、dest、mp分別是%1柳洋、%2叹坦、%3、%4募书。
因此,cmpxchgl %1,(%3)實(shí)際上表示cmpxchgl exchange_value,(dest)莹捡,此處(dest)表示dest地址所存的值。需要注意的是cmpxchgl有個(gè)隱含操作數(shù)eax篮赢,其實(shí)際過(guò)程是先比較eax的值(也就是compare_value)和dest地址所存的值是否相等,如果相等則把exchange_value的值寫(xiě)入dest指向的地址启泣。如果不相等則把dest地址所存的值存入eax中。
輸出是"=a" (exchange_value)寥茫,表示把eax中存的值寫(xiě)入exchange_value變量中。
Atomic::cmpxchg這個(gè)函數(shù)最終返回值是exchange_value,也就是說(shuō)险耀,如果cmpxchgl執(zhí)行時(shí)compare_value和dest指針指向內(nèi)存值相等則會(huì)使得dest指針指向內(nèi)存值變成exchange_value,最終eax存的compare_value賦值給了exchange_value變量甩牺,即函數(shù)最終返回的值是原先的compare_value。此時(shí)Unsafe_CompareAndSwapInt的返回值(jint)(Atomic::cmpxchg(x, addr, e)) == e就是true柴灯,表明CAS成功。如果cmpxchgl執(zhí)行時(shí)compare_value和(dest)不等則會(huì)把當(dāng)前dest指針指向內(nèi)存的值寫(xiě)入eax赠群,最終輸出時(shí)賦值給exchange_value變量作為返回值,導(dǎo)致(jint)(Atomic::cmpxchg(x, addr, e)) == e得到false查描,表明CAS失敗。
假設(shè)原值為old冬三,存在ptr所執(zhí)行的位置,想寫(xiě)入新值new勾笆,那么cmpxchg實(shí)現(xiàn)的功能就是比較old和ptr指向的內(nèi)容,如果相等則ptr所指地址寫(xiě)入new窝爪,然后返回old亭敢,如果不相等則把ptr當(dāng)前所指向地址存的值返回衙伶。(上面的沒(méi)看懂沒(méi)關(guān)系妖异,記住這個(gè)結(jié)論就行了邀杏,或者你可以選擇看看第5部分)
5.比較并交換 Compare-and-swap
第4部分唬血,我們從Java一直探究到機(jī)器指令cmpxchgl嘗試來(lái)搞懂Java的CAS是如何實(shí)現(xiàn)的。由于已經(jīng)是機(jī)器指令了刁品,所以任何一門(mén)編程語(yǔ)言都可能使用它,所以我們?cè)趶挠?jì)算機(jī)科學(xué)的角度來(lái)看看比較并交換挑随,從而做到舉一反三勒叠。下面的內(nèi)容主要來(lái)自維基百科。
比較并交換(compare and swap,
CAS)眯分,是原子操作的一種,可用于在多線(xiàn)程編程中實(shí)現(xiàn)不被打斷的數(shù)據(jù)交換操作弊决,從而避免多線(xiàn)程同時(shí)改寫(xiě)某一數(shù)據(jù)時(shí)由于執(zhí)行順序不確定性以及中斷的不可預(yù)知性產(chǎn)生的數(shù)據(jù)不一致問(wèn)題。
該操作通過(guò)將內(nèi)存中的值與指定數(shù)據(jù)進(jìn)行比較飘诗,當(dāng)數(shù)值一樣時(shí)將內(nèi)存中的數(shù)據(jù)替換為新的值。
一個(gè)CAS操作的過(guò)程可以用以下c代碼表示:
int cas(long *addr, long old, long new)
{
/* Executes atomically. */
if(*addr != old)
return 0;
*addr = new;
return 1;
}
在使用上昆稿,通常會(huì)記錄下某塊內(nèi)存中的舊值,通過(guò)對(duì)舊值進(jìn)行一系列的操作后得到新值溉潭,然后通過(guò)CAS操作將新值與舊值進(jìn)行交換。如果這塊內(nèi)存的值在這期間內(nèi)沒(méi)被修改過(guò)喳瓣,則舊值會(huì)與內(nèi)存中的數(shù)據(jù)相同,這時(shí)CAS操作將會(huì)成功執(zhí)行使內(nèi)存中的數(shù)據(jù)變?yōu)樾轮滴飞隆H绻麅?nèi)存中的值在這期間內(nèi)被修改過(guò),則一般來(lái)說(shuō)舊值會(huì)與內(nèi)存中的數(shù)據(jù)不同仿滔,這時(shí)CAS操作將會(huì)失敗,新值將不會(huì)被寫(xiě)入內(nèi)存堤撵。
6.總結(jié):
從Java里的Unsafe.java的compareAndSwapInt
方法实昨,再到C++下的unsafe.cpp的Unsafe_CompareAndSwapInt,
再到CPU指令 lock cmpxchgl,
可以看到編程語(yǔ)言從Java,變到C/C++, 再到CPU指令荒给,這真是一次奇妙的旅程。
一方面志电,Java幫程我們層層封裝,不用再去擔(dān)心底層的區(qū)別蛔趴,不用再去擔(dān)心如何維護(hù)內(nèi)存,如何使用指針等等,你只需要好好地實(shí)現(xiàn)上層的應(yīng)用洒嗤。
另一方面,天下沒(méi)有免費(fèi)的午餐渔隶,出來(lái)混早晚都要還的,既然底層是這些語(yǔ)言實(shí)現(xiàn)的间唉,當(dāng)別人問(wèn)到你時(shí),你要么說(shuō)不會(huì)呈野,要么就得一層層看下去,最起碼是要理解最關(guān)鍵的部分际跪。
當(dāng)然,從CPU指令到匯編喉钢,到C/C++, 再到Java,一層層抽象本意就是讓編程語(yǔ)言越來(lái)越好用肠虽,但為了要走的更遠(yuǎn),你就得記得自己從哪里來(lái)税课。
另外比較并交換(compare and set CAS)還會(huì)遇到ABA問(wèn)題,我們放到下一節(jié)講Java原子類(lèi)的時(shí)候一并說(shuō)明韩玩。
7.參考:
https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
https://en.cppreference.com/w/cpp/language/asm
https://blog.csdn.net/prstaxy/article/details/51802220
https://en.wikipedia.org/wiki/Compare-and-swap
https://zh.wikipedia.org/wiki/%E6%AF%94%E8%BE%83%E5%B9%B6%E4%BA%A4%E6%8D%A2