0.Unsafe介紹
JavaDoc說, Unsafe提供了一組用于執(zhí)行底層的,不安全操作的方法。那么具體有哪些方法呢,我畫了一張圖。
可以看到Unsafe中提供了CAS,內(nèi)存操作,線程調(diào)度,本機(jī)信息,Class相關(guān)方法,查看和設(shè)置某個(gè)對象或字段,內(nèi)存分配和釋放相關(guān)操作,內(nèi)存地址獲取相關(guān)方法。我自己抽空對上述方法進(jìn)行了注釋,
你可以在這里看到。
那么如何使用Unsafe呢?下面我們就來說說如何獲取Unsafe并操作。
1.獲取Unsafe實(shí)例
如下所述,由于Unsafe.getUnsafe會(huì)判斷調(diào)用類的類加載器是否為引導(dǎo)類加載器,如果是,可以正常獲取Unsafe實(shí)例,否則會(huì)拋出安全異常。
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
主要有兩種方式來繞過安全檢查,一種是通過將使用Unsafe的類交給bootstrap class loader去加載,另一種方式是通過反射。
1.1 通過bootstrap class loader去加載Unsafe。
public class GetUnsafeFromMethod {
public static void main(String[] args){
//調(diào)用這個(gè)方法,必須要在啟動(dòng)類加載器中獲取,否則會(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的類加載器是應(yīng)用類加載器(Application Class loader),而要求的是啟動(dòng)類加載器,
因而!VM.isSystemDomainLoader(var0.getClassLoader()) 返回false,拋出異常。
但是通過下面的命令行,我們把GetUnsafeFromMethod.java 追加到bootclasspath(啟動(dòng)類加載路徑)上,就可以正常執(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)都要加這么一大串指令,所以下面我們就來反射是不是好用些。
1.2 通過反射獲取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;
}
}
嗯,通過反射就可以直接用了,是不是比上一種利用啟動(dòng)類加載器加載的方式好用很多
3.Unsafe API 的使用
具體如何使用,可以查看這篇文章。
實(shí)際的應(yīng)用案例,可以查看美團(tuán)的一篇文章。
4. Unsafe 中CAS部分的實(shí)現(xiàn)
我們可以看到Unsafe中基本都是調(diào)用native方法,如果你比較好奇這個(gè)native方法又是如何實(shí)現(xiàn)的,那么就需要去JVM里面找對應(yīng)的實(shí)現(xiàn)。
到http://hg.openjdk.java.net/ 進(jìn)行一步步選擇下載對應(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è)就是對應(yīng)jvm相關(guān)的c++實(shí)現(xiàn)類了。
比如我們對CAS部分的實(shí)現(xiàn)很感興趣,就可以在該文件中搜索compareAndSwapInt,此時(shí)可以看到對應(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)}
...
};
接著我們在搜索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); //查找要指定的對象
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); //獲取要操作的是對象的字段的內(nèi)存地址。
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //執(zhí)行Atomic類中的cmpxchg。
UNSAFE_END
可以看到最后會(huì)調(diào)用到Atomic::cmpxchg里面的函數(shù),這個(gè)根據(jù)不同操作系統(tǒng)和不同CPU會(huì)有不同的實(shí)現(xiàn),但都放在hotspot\src\os_cpu 目錄下,比如linux_64x的,對應(yīng)類就是hotspot\src\os_cpu\linux_x86\vm\atomic_linux_x86.inline.hpp, 而windows_64x的,對應(yīng)類就是hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp。(此處也說明了為什么Java可以Write once, Run everywhere, 原因就是JVM源碼對不同操作系統(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__: 表示接下來是內(nèi)聯(lián)的匯編代碼,這里使用asm語句可以將匯編指令直接包含在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)是否為多核處理器,來決定是否為cmpxchg指令添加lock前綴。如果有l(wèi)ock前綴的話,則會(huì)根據(jù)CPU不同會(huì)采用鎖總線或者鎖cache line的方式,來實(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í)行過程:
首先,輸入是"r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp),表示compare_value存入eax寄存器,而exchange_value、dest、mp的值存入任意的通用寄存器。嵌入式匯編規(guī)定把輸出和輸入寄存器按統(tǒng)一順序編號,順序是從輸出寄存器序列從左到右從上到下以“%0”開始,分別記為%0、%1···%9。也就是說,輸出的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í)際過程是先比較eax的值(也就是compare_value)和dest地址所存的值是否相等,如果相等則把exchange_value的值寫入dest指向的地址。如果不相等則把dest地址所存的值存入eax中。
輸出是"=a" (exchange_value),表示把eax中存的值寫入exchange_value變量中。
Atomic::cmpxchg這個(gè)函數(shù)最終返回值是exchange_value,也就是說,如果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)存的值寫入eax,最終輸出時(shí)賦值給exchange_value變量作為返回值,導(dǎo)致(jint)(Atomic::cmpxchg(x, addr, e)) == e得到false,表明CAS失敗。
假設(shè)原值為old,存在ptr所執(zhí)行的位置,想寫入新值new,那么cmpxchg實(shí)現(xiàn)的功能就是比較old和ptr指向的內(nèi)容,如果相等則ptr所指地址寫入new,然后返回old,如果不相等則把ptr當(dāng)前所指向地址存的值返回。(上面的沒看懂沒關(guān)系,記住這個(gè)結(jié)論就行了,或者你可以選擇看看第5部分)
5.比較并交換 Compare-and-swap
第4部分,我們從Java一直探究到機(jī)器指令cmpxchgl嘗試來搞懂Java的CAS是如何實(shí)現(xiàn)的。由于已經(jīng)是機(jī)器指令了,所以任何一門編程語言都可能使用它,所以我們在從計(jì)算機(jī)科學(xué)的角度來看看比較并交換,從而做到舉一反三。下面的內(nèi)容主要來自維基百科。
比較并交換(compare and swap,
CAS),是原子操作的一種,可用于在多線程編程中實(shí)現(xiàn)不被打斷的數(shù)據(jù)交換操作,從而避免多線程同時(shí)改寫某一數(shù)據(jù)時(shí)由于執(zhí)行順序不確定性以及中斷的不可預(yù)知性產(chǎn)生的數(shù)據(jù)不一致問題。
該操作通過將內(nèi)存中的值與指定數(shù)據(jù)進(jìn)行比較,當(dāng)數(shù)值一樣時(shí)將內(nèi)存中的數(shù)據(jù)替換為新的值。
一個(gè)CAS操作的過程可以用以下c代碼表示:
int cas(long *addr, long old, long new)
{
/* Executes atomically. */
if(*addr != old)
return 0;
*addr = new;
return 1;
}
在使用上,通常會(huì)記錄下某塊內(nèi)存中的舊值,通過對舊值進(jìn)行一系列的操作后得到新值,然后通過CAS操作將新值與舊值進(jìn)行交換。如果這塊內(nèi)存的值在這期間內(nèi)沒被修改過,則舊值會(huì)與內(nèi)存中的數(shù)據(jù)相同,這時(shí)CAS操作將會(huì)成功執(zhí)行使內(nèi)存中的數(shù)據(jù)變?yōu)樾轮?。如果?nèi)存中的值在這期間內(nèi)被修改過,則一般來說舊值會(huì)與內(nèi)存中的數(shù)據(jù)不同,這時(shí)CAS操作將會(huì)失敗,新值將不會(huì)被寫入內(nèi)存。
6.總結(jié):
從Java里的Unsafe.java的compareAndSwapInt方法,再到C++下的unsafe.cpp的Unsafe_CompareAndSwapInt, 再到CPU指令 lock cmpxchgl, 可以看到編程語言從Java,變到C/C++, 再到CPU指令,這真是一次奇妙的旅程。
一方面,Java幫程我們層層封裝,不用再去擔(dān)心底層的區(qū)別,不用再去擔(dān)心如何維護(hù)內(nèi)存,如何使用指針等等,你只需要好好地實(shí)現(xiàn)上層的應(yīng)用。
另一方面,天下沒有免費(fèi)的午餐,出來混早晚都要還的,既然底層是這些語言實(shí)現(xiàn)的,當(dāng)別人問到你時(shí),你要么說不會(huì),要么就得一層層看下去,最起碼是要理解最關(guān)鍵的部分。
當(dāng)然,從CPU指令到匯編,到C/C++, 再到Java,一層層抽象本意就是讓編程語言越來越好用,但為了要走的更遠(yuǎn),你就得記得自己從哪里來。
另外比較并交換(compare and set CAS)還會(huì)遇到ABA問題,我們放到下一節(jié)講Java原子類的時(shí)候一并說明。
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