1. 計算機(jī)原理
? Java內(nèi)存模型即Java Memory Model店茶,簡稱JMM建蹄。JMM定義了Java 虛擬機(jī)(JVM)在計算機(jī)內(nèi)存(RAM)中的工作方式茴扁。JVM是整個計算機(jī)虛擬模型登下,所以JMM是隸屬于JVM的卓练。Java1.5版本對其進(jìn)行了重構(gòu),現(xiàn)在的Java仍沿用了Java1.5的版本晒哄。
根據(jù)《Jeff Dean在Google全體工程大會的報告》我們可以看到
計算機(jī)在做一些我們平時的基本操作時泉蝌,需要的響應(yīng)時間是不一樣的。
2. Java內(nèi)存模型
? 從抽象的角度來看揩晴,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(Main Memory)中勋陪,每個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本硫兰。本地內(nèi)存是JMM的一個抽象概念诅愚,并不真實存在。它涵蓋了緩存劫映、寫緩沖區(qū)违孝、寄存器以及其他的硬件和編譯器優(yōu)化。
2.1 可見性
? 可見性是指當(dāng)多個線程訪問同一個變量時泳赋,一個線程修改了這個變量的值雌桑,其他線程能夠立即看得到修改的值。
? 由于線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行祖今,而不能直接讀寫主內(nèi)存中的變量校坑,那么對于共享變量V拣技,它們首先是在自己的工作內(nèi)存,之后再同步到主內(nèi)存耍目「嘟铮可是并不會及時的刷到主存中,而是會有一定時間差邪驮。很明顯莫辨,這個時候線程 A 對變量 V 的操作對于線程 B 而言就不具備可見性了 。
? 要解決共享對象可見性這個問題毅访,我們可以使用volatile關(guān)鍵字或者是加鎖沮榜。
2.2 原子性
? 原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行喻粹。
? 我們都知道CPU資源的分配都是以線程為單位的,并且是分時調(diào)用,操作系統(tǒng)允許某個進(jìn)程執(zhí)行一小段時間蟆融,例如 50 毫秒,過了 50 毫秒操作系統(tǒng)就會重新選擇一個進(jìn)程來執(zhí)行(我們稱為“任務(wù)切換”)磷斧,這個 50 毫秒稱為“時間片”振愿。而任務(wù)的切換大多數(shù)是在時間片段結(jié)束以后,
? 那么線程切換為什么會帶來bug呢捷犹?因為操作系統(tǒng)做任務(wù)切換弛饭,可以發(fā)生在任何一條CPU 指令執(zhí)行完!注意萍歉,是 CPU 指令,而不是高級語言里的一條語句侣颂。比如count++,在java里就是一句話枪孩,但高級語言里一條語句往往需要多條 CPU 指令完成憔晒。其實count++包含了三個CPU指令!
3.volatile詳解
3.1 volatile特性
? 可見性蔑舞。對一個volatile變量的讀拒担,總是能看到(任意線程)對這個volatile變量最后的寫入。
? 原子性:對任意單個volatile變量的讀/寫具有原子性攻询,但類似于volatile++這種復(fù)合操作不具有原子性从撼。例如:volatile雖然能保證執(zhí)行完及時把變量刷到主內(nèi)存中,但對于count++這種非原子性钧栖、多指令的情況低零,由于線程切換,線程A剛把count=0加載到工作內(nèi)存拯杠,線程B就可以開始工作了掏婶,這樣就會導(dǎo)致線程A和B執(zhí)行完的結(jié)果都是1,都寫到主內(nèi)存中潭陪,主內(nèi)存的值還是1不是2雄妥。
? 抑制重排序:有volatile修飾的變量最蕾,賦值后多執(zhí)行了一個“l(fā)oad addl $0x0, (%esp)”操作,這個操作相當(dāng)于一個內(nèi)存屏障(指令重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置)茎芭。
? 指令重排序:處理器為了提高程序運行效率揖膜,可能會對輸入代碼進(jìn)行優(yōu)化适揉,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致叠聋,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
3.2 volatile的實現(xiàn)原理
? 為了探究Volatile的底層實現(xiàn)原理呻此,我們需要先將java代碼編程成字節(jié)碼宿百,然后通過java工具看匯編代碼就可以知道底層原理趁仙。下面來看看
public class TestVolatile {
private static volatile int i = 0;
public static void main(String[] args) {
}
}
使用javap -c 文件名 或者AndroidStudio的ASM插件看字節(jié)碼
static <clinit>()V
L0
LINENUMBER 8 L0
ICONST_0
// 下面這行字節(jié)碼無論是否使用volatile修飾,都是一樣的
PUTSTATIC com/example/threadtest/volatilesynchronized/TestVolatile.i : I
RETURN
MAXSTACK = 1
MAXLOCALS = 0
然后繼續(xù)往下探索
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly TestVolatile > 1.txt
查看匯編碼
Line 2521: 0x000000000356fecc: lock cmpxchg qword ptr [rdx],rdi
Line 2528: 0x000000000356feed: lock cmpxchg qword ptr [rdx],rdi
Line 2534: 0x000000000356ff0b: lock add dword ptr [rsp],0h ;*putstatic
Line 2538: 0x000000000356ff1a: lock cmpxchg qword ptr [rdx],rsi
Line 2816: 0x0000000003570338: lock cmpxchg qword ptr [rdi],rsi
Line 2941: 0x0000000003570479: lock cmpxchg qword ptr [rsi],rdi
可以看到
匯編指令中會有很多加“l(fā)ock”前綴的指令垦页。
加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時所生成的匯編代碼發(fā)現(xiàn)雀费,加入volatile關(guān)鍵字時,會多出一個lock前綴指令痊焊。我們發(fā)現(xiàn)盏袄,volatile變量在字節(jié)碼級別沒有任何區(qū)別,在匯編級別使用了lock指令前綴薄啥。
lock是一個指令前綴辕羽,Intel的手冊上對其的解釋是:
Causes the processor's LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal insures that the processor has exclusive use of any shared memory while the signal is asserted.
? 簡單理解也就是說,lock后就是一個原子操作垄惧。原子操作是指不會被線程調(diào)度機(jī)制打斷的操作刁愿;這種操作一旦開始,就一直運行到結(jié)束到逊,中間不會有任何 context switch (切換到另一個線程)铣口。
? 當(dāng)使用 LOCK 指令前綴時,它會使 CPU 宣告一個 LOCK# 信號觉壶,這樣就能確保在多處理器系統(tǒng)或多線程競爭的環(huán)境下互斥地使用這個內(nèi)存地址脑题。當(dāng)指令執(zhí)行完畢,這個鎖定動作也就會消失铜靶。
? 乍一看是不是感覺和Java的synchronized鎖叔遂。但volatile底層使用多核處理器實現(xiàn)的lock指令,更底層旷坦,消耗代價更小掏熬。
? 因此有人將Java的synchronized看作重量級的鎖,而volatile看作輕量級的鎖 并不是全無道理秒梅。
? lock前綴指令其實就相當(dāng)于一個內(nèi)存屏障旗芬。內(nèi)存屏障是一組CPU處理指令,用來實現(xiàn)對內(nèi)存操作的順序限制捆蜀。volatile的底層就是通過內(nèi)存屏障來實現(xiàn)的疮丛。
? 編譯器和執(zhí)行器 可以在保證輸出結(jié)果一樣的情況下對指令重排序幔嫂,使性能得到優(yōu)化。插入一個內(nèi)存屏障誊薄,相當(dāng)于告訴CPU和編譯器先于這個命令的必須先執(zhí)行履恩,后于這個命令的必須后執(zhí)行。
? 內(nèi)存屏障另一個作用是強(qiáng)制更新一次不同CPU的緩存呢蔫。例如切心,一個寫屏障會把這個屏障前寫入的數(shù)據(jù)刷新到緩存,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值片吊,而不用考慮到底是被哪個cpu核心或者哪個CPU執(zhí)行的绽昏。這正是volatile實現(xiàn)內(nèi)存可見性的基礎(chǔ)。
3.3 synchronized的實現(xiàn)原理
? Synchronized在JVM里的實現(xiàn)都是基于進(jìn)入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步俏脊,雖然具體實現(xiàn)細(xì)節(jié)不一樣全谤,但是都可以通過成對的MonitorEnter和MonitorExit指令來實現(xiàn)。
// 測試代碼
public class Test {
private static volatile int i = 0;
private final Object lock = new Object();
public void a() {
synchronized (lock) {
i++;
}
}
public synchronized void b() {
i++;
}
}
? 對同步塊爷贫,MonitorEnter指令插入在同步代碼塊的開始位置认然,當(dāng)代碼執(zhí)行到該指令時,將會嘗試獲取該對象Monitor的所有權(quán)漫萄,即嘗試獲得該對象的鎖卷员,而monitorExit指令則插入在方法結(jié)束處和異常處,JVM保證每個MonitorEnter必須有對應(yīng)的MonitorExit卷胯。
public a()V
......
DUP
ASTORE 1
MONITORENTER // 同步塊開始
L0
LINENUMBER 15 L0
GETSTATIC com/example/threadtest/volatilesynchronized/Test.i : I
ICONST_1
IADD
PUTSTATIC com/example/threadtest/volatilesynchronized/Test.i : I
L5
LINENUMBER 16 L5
ALOAD 1
MONITOREXIT // 同步塊結(jié)束
L1
GOTO L6
......
? 對同步方法子刮,從同步方法反編譯的結(jié)果來看威酒,方法的同步并沒有通過指令monitorenter和monitorexit來實現(xiàn)窑睁,相對于普通方法,其常量池中多了ACC_SYNCHRONIZED標(biāo)示符葵孤。
// access flags 0x1 沒有synchronized修飾
public a()V
// access flags 0x21 synchronized修飾
public synchronized b()V
? JVM就是根據(jù)該標(biāo)示符來實現(xiàn)方法的同步的:當(dāng)方法被調(diào)用時担钮,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了尤仍,執(zhí)行線程將先獲取monitor箫津,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor宰啦。在方法執(zhí)行期間苏遥,其他任何線程都無法再獲得同一個monitor對象。
? synchronized使用的鎖信息是存放在Java對象頭里面赡模。
? 具體位置是對象頭里面的MarkWord田炭,MarkWord里默認(rèn)數(shù)據(jù)是存儲對象的HashCode等信息,
? 但是會隨著對象的運行改變而發(fā)生變化漓柑,不同的鎖狀態(tài)對應(yīng)著不同的記錄存儲方式
3.4 各種鎖
3.4.1.1 自旋鎖 (輕量級鎖)
? 自旋鎖原理非常簡單教硫,如果持有鎖的線程能在很短時間內(nèi)釋放鎖資源叨吮,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞掛起狀態(tài),它們只需要等一等(自旋)瞬矩,等持有鎖的線程釋放鎖后即可立即獲取鎖茶鉴,這樣就避免用戶線程和內(nèi)核的切換的消耗。
3.4.1.2 自旋鎖的優(yōu)缺點
? 自旋鎖盡可能的減少線程的阻塞景用,這對于鎖的競爭不激烈涵叮,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小于線程阻塞掛起操作的消耗伞插。
? 但是如果鎖的競爭激烈围肥,或者持有鎖的線程需要長時間占用鎖執(zhí)行同步塊,這時候就不適合使用自旋鎖了蜂怎,因為自旋鎖在獲取鎖前一直都是占用cpu做無用功穆刻。
3.4.1.3 自旋鎖時間閾值
? JVM對于自旋次數(shù)的選擇,jdk1.5默認(rèn)為10次杠步,在1.6引入了適應(yīng)性自旋鎖
氢伟,適應(yīng)性自旋鎖意味著自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態(tài)來決定幽歼,基本認(rèn)為一個線程上下文切換的時間是最佳的一個時間朵锣。
? JDK1.6中-XX:+UseSpinning開啟自旋鎖; JDK1.7后甸私,去掉此參數(shù)诚些,由jvm控制
3.4.2.1 偏向鎖
? 研究人員經(jīng)過大量統(tǒng)計,發(fā)現(xiàn)一把鎖總是被同一線程競爭拿到皇型,所以就引入偏向鎖概念诬烹,當(dāng)在進(jìn)行CAS自旋拿鎖之前,先判斷自己是不是在競爭這把鎖弃鸦,如果是就直接執(zhí)行绞吁。
? 偏向鎖,顧名思義唬格,它會偏向于第一個訪問鎖的線程家破,如果在運行過程中,同步鎖只有一個線程訪問购岗,不存在多線程爭用的情況汰聋,則線程是不需要觸發(fā)同步的,減少加鎖/解鎖的一些CAS操作(比如等待隊列的一些CAS操作)喊积,這種情況下烹困,就會給線程加一個偏向鎖。 如果在運行過程中注服,遇到了其他線程搶占鎖韭邓,則持有偏向鎖的線程會被掛起措近,JVM會消除它身上的偏向鎖,將鎖恢復(fù)到標(biāo)準(zhǔn)的輕量級鎖女淑。它通過消除資源無競爭情況下的同步原語瞭郑,進(jìn)一步提高了程序的運行性能。
偏向鎖獲取過程:
第一步鸭你,訪問Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1屈张,鎖標(biāo)志位是否為01,確認(rèn)為可偏向狀態(tài)袱巨。
第二步阁谆,如果為可偏向狀態(tài),則測試線程ID是否指向當(dāng)前線程愉老,如果是场绿,進(jìn)入步驟5,否則進(jìn)入步驟3嫉入。
第三步焰盗,如果線程ID并未指向當(dāng)前線程,則通過CAS操作競爭鎖咒林。如果競爭成功熬拒,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID,然后執(zhí)行5垫竞;如果競爭失敗澎粟,執(zhí)行4。
第四步欢瞪,如果CAS獲取偏向鎖失敗活烙,則表示有競爭。當(dāng)?shù)竭_(dá)全局安全點(safepoint)時獲得偏向鎖的線程被掛起引有,偏向鎖升級為輕量級鎖瓣颅,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼倦逐。(撤銷偏向鎖的時候會導(dǎo)致stop the word)
第五步譬正,執(zhí)行同步代碼。
偏向鎖的釋放:
? 偏向鎖的撤銷在上述第四步驟中有提到檬姥。偏向鎖只有遇到其他線程嘗試競爭偏向鎖時曾我,持有偏向鎖的線程才會釋放偏向鎖,線程不會主動去釋放偏向鎖健民。偏向鎖的撤銷抒巢,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程秉犹,判斷鎖對象是否處于被鎖定狀態(tài)蛉谜,撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為“01”)或輕量級鎖(標(biāo)志位為“00”)的狀態(tài)稚晚。
偏向鎖的適用場景
? 始終只有一個線程在執(zhí)行同步塊,在它沒有執(zhí)行完釋放鎖之前型诚,沒有其它線程去執(zhí)行同步塊客燕,在鎖無競爭的情況下使用,一旦有了競爭就升級為輕量級鎖狰贯,升級為輕量級鎖的時候需要撤銷偏向鎖也搓,撤銷偏向鎖的時候會導(dǎo)致stop the word操作;
? 在有鎖的競爭時涵紊,偏向鎖會多做很多額外操作傍妒,尤其是撤銷偏向所的時候會導(dǎo)致進(jìn)入安全點,安全點會導(dǎo)致stw摸柄,導(dǎo)致性能下降颤练,這種情況下應(yīng)當(dāng)禁用。
jvm開啟/關(guān)閉偏向鎖
開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
關(guān)閉偏向鎖:-XX:-UseBiasedLocking
3.4.3 輕量級鎖
? 輕量級鎖是由偏向鎖升級來的驱负,偏向鎖運行在一個線程進(jìn)入同步塊的情況下昔案,當(dāng)?shù)诙€線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖电媳;
輕量級鎖的加鎖過程:
? 在代碼進(jìn)入同步塊的時候踏揣,如果同步對象鎖狀態(tài)為無鎖狀態(tài)且不允許進(jìn)行偏向(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”)匾乓,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間捞稿,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word拼缝。
? 拷貝成功后娱局,虛擬機(jī)將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock record里的owner指針指向object mark word咧七。如果更新成功衰齐,則執(zhí)行步驟4,否則執(zhí)行步驟5继阻。
如果這個更新動作成功了耻涛,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志位設(shè)置為“00”瘟檩,即表示此對象處于輕量級鎖定狀態(tài)
如果這個更新操作失敗了抹缕,虛擬機(jī)首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖墨辛,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行卓研。否則說明多個線程競爭鎖,當(dāng)競爭線程嘗試占用輕量級鎖失敗多次之后,輕量級鎖就會膨脹為重量級鎖奏赘,重量級線程指針指向競爭線程寥闪,競爭線程也會阻塞,等待輕量級線程釋放鎖后喚醒他磨淌。鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”橙垢,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)伦糯。
3.4.4 不同鎖之間的區(qū)別
? JVM虛擬機(jī)對synchronized關(guān)鍵字的優(yōu)化引入了自適應(yīng)性CAS鎖柜某、偏向鎖和輕量級鎖機(jī)制。