Unsafe介紹及CAS原理解析

0.Unsafe介紹

JavaDoc說(shuō), Unsafe提供了一組用于執(zhí)行底層的,不安全操作的方法缔莲。那么具體有哪些方法呢,我畫(huà)了一張圖痴奏。

Unsafe 方法分類(lèi)

可以看到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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市合愈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌佛析,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寸莫,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡膘茎,警方通過(guò)查閱死者的電腦和手機(jī)桃纯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)慈参,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人驮配,你說(shuō)我怎么就攤上這事∽湃祝” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵猜绣,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我敬特,道長(zhǎng),這世上最難降的妖魔是什么伟阔? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮怀估,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘合搅。我一直安慰自己,他們只是感情好灾部,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著赌髓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪春弥。 梳的紋絲不亂的頭發(fā)上叠荠,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音榛鼎,去河邊找鬼鳖孤。 笑死抡笼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的推姻。 我是一名探鬼主播平匈,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼增炭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了隙姿?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤输玷,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后欲鹏,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡貌虾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年裙犹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叶圃。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖掺冠,靈堂內(nèi)的尸體忽然破棺而出沉馆,到底是詐尸還是另有隱情德崭,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布眉厨,位于F島的核電站,受9級(jí)特大地震影響憾股,放射性物質(zhì)發(fā)生泄漏箕慧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一颠焦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧伐庭,春花似錦、人聲如沸婉商。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蘑秽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肠牲,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工缀雳, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肥印。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像深碱,于是被迫代替她去往敵國(guó)和親腹鹉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子敷硅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354