深度剖析 synchronized
線程安全是并發(fā)編程中關(guān)注的重點(diǎn),應(yīng)該注意到的是,造成線程安全問(wèn)題的主要原因有兩點(diǎn),一是存在共享數(shù)據(jù)(也稱臨界資源),二是存在多條線程共同操作共享數(shù)據(jù)。因此為了解決這個(gè)問(wèn)題,Java 引入了互斥鎖的概念,對(duì)共享數(shù)據(jù)變量在訪問(wèn)前需要獲取鎖,然后才能對(duì)其進(jìn)行修改,修改完后再釋放鎖,沒(méi)有獲取到鎖的線程只能等待,直到當(dāng)前線程處理完畢釋放該鎖。這樣能夠保證在同一時(shí)刻只有一個(gè)線程能夠?qū)蚕頂?shù)據(jù)進(jìn)行操作,保證了多線程下的線程安全。在 Java 中,關(guān)鍵字 synchronized 可以保證在同一個(gè)時(shí)刻,只有一個(gè)線程可以執(zhí)行某個(gè)方法或者某個(gè)代碼塊(主要是對(duì)方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時(shí)我們還應(yīng)該注意到synchronized 另外一個(gè)重要的作用,synchronized 可保證一個(gè)線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到。
synchronized 底層原理
Java 虛擬機(jī)中的同步是基于進(jìn)入和退出管程(Monitor)對(duì)象實(shí)現(xiàn),無(wú)論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)還是隱式同步都是如此。在 Java 語(yǔ)言中,同步用的最多的地方可能是被 synchronized 修飾的同步方法。同步方法并不是由 monitorenter 和 monitorexit 指令來(lái)實(shí)現(xiàn)同步的,而是由方法調(diào)用指令讀取運(yùn)行時(shí)常量池中方法的 ACC_SYNCHRONIZED 標(biāo)志來(lái)隱式實(shí)現(xiàn)的,關(guān)于這點(diǎn),稍后詳細(xì)分析。下面先來(lái)了解一個(gè)概念Java對(duì)象頭,這對(duì)深入理解 synchronized 實(shí)現(xiàn)原理非常關(guān)鍵。
Java 對(duì)象頭與 Monitor
在 JVM 中,對(duì)象在內(nèi)存中的布局分為三塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。這三塊區(qū)域的作用分別是:
-
實(shí)例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息,如果是數(shù)組的實(shí)例部分還包括數(shù)組的長(zhǎng)度,這部分內(nèi)存按4字節(jié)對(duì)齊。
-
填充數(shù)據(jù):由于虛擬機(jī)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對(duì)齊。
-
對(duì)象頭包括兩部分:Mark Word 和 類型指針。
-
Mark WordMark Word用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等等,占用內(nèi)存大小與虛擬機(jī)位長(zhǎng)一致。
-
類型指針類型指針指向?qū)ο蟮念愒獢?shù)據(jù),虛擬機(jī)通過(guò)這個(gè)指針確定該對(duì)象是哪個(gè)類的實(shí)例。
-
Mark Word記錄了對(duì)象和鎖有關(guān)的信息,當(dāng)這個(gè)對(duì)象被 synchronized 關(guān)鍵字當(dāng)成同步鎖時(shí),圍繞這個(gè)鎖的一系列操作都和 Mark Word 有關(guān)。 Mark Word在32位JVM中的長(zhǎng)度是32bit,在64位JVM中長(zhǎng)度是64bit。 下面附上 openjdk 關(guān)于對(duì)象頭 markword 描述的源碼說(shuō)明,markword 的結(jié)構(gòu),定義在 markOop.hpp 文件:
//?32?bits: //?-------- //?hash:25?------------>|?age:4????biased_lock:1?lock:2?(normal?object) //?JavaThread*:23?epoch:2?age:4????biased_lock:1?lock:2?(biased?object) //?size:32?------------------------------------------>|?(CMS?free?block) //?PromotedObject*:29?---------->|?promo_bits:3?----->|?(CMS?promoted?object) // //?64?bits: //?-------- //?unused:25?hash:31?-->|?unused:1???age:4????biased_lock:1?lock:2?(normal?object) //?JavaThread*:54?epoch:2?unused:1???age:4????biased_lock:1?lock:2?(biased?object) //?PromotedObject*:61?--------------------->|?promo_bits:3?----->|?(CMS?promoted?object) //?size:64?----------------------------------------------------->|?(CMS?free?block) // //?unused:25?hash:31?-->|?cms_free:1?age:4????biased_lock:1?lock:2?(COOPs?&&?normal?object) //?JavaThread*:54?epoch:2?cms_free:1?age:4????biased_lock:1?lock:2?(COOPs?&&?biased?object) //?narrowOop:32?unused:24?cms_free:1?unused:4?promo_bits:3?----->|?(COOPs?&&?CMS?promoted?object) //?unused:21?size:35?-->|?cms_free:1?unused:7?------------------>|?(COOPs?&&?CMS?free?block) //[ptr?????????????|?00]??locked?????????????ptr?points?to?real?header?on?stack //[header??????|?0?|?01]??unlocked???????????regular?object?header //[ptr?????????????|?10]??monitor????????????inflated?lock?(header?is?wapped?out) //[ptr?????????????|?11]??marked?????????????used?by?markSweep?to?mark?an?object64 位的 Mark Word 示意圖
其中無(wú)鎖和偏向鎖的鎖標(biāo)志位都是01,只是在前面的1bit區(qū)分了這是無(wú)鎖狀態(tài)還是偏向鎖狀態(tài)。輕量級(jí)鎖是 00,重量級(jí)鎖是 10。
JDK1.6以后的版本在處理同步鎖時(shí)存在鎖升級(jí)的概念,JVM對(duì)于同步鎖的處理是從偏向鎖開(kāi)始的,隨著競(jìng)爭(zhēng)越來(lái)越激烈,處理方式從偏向鎖升級(jí)到輕量級(jí)鎖,最終升級(jí)到重量級(jí)鎖。
其中輕量級(jí)鎖和偏向鎖是 Java 6 對(duì) synchronized 鎖進(jìn)行優(yōu)化后新增加的,稍后會(huì)進(jìn)行分析。這里我們主要分析一下重量級(jí)鎖也就是通常說(shuō) synchronized 的對(duì)象鎖,鎖標(biāo)識(shí)位為10,其中指針指向的是 monitor 對(duì)象的起始地址。當(dāng)一個(gè) monitor 被某個(gè)線程持有后,它便處于鎖定狀態(tài)。在Java虛擬機(jī)(HotSpot)中,monitor 是由 ObjectMonitor 實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于 HotSpot 虛擬機(jī)源碼 ObjectMonitor.hpp 文件,C++ 實(shí)現(xiàn)的)
ObjectMonitor()?{_header???????=?NULL;_count????????=?0;?//記錄個(gè)數(shù)_waiters??????=?0,_recursions???=?0;_object???????=?NULL;_owner????????=?NULL;_WaitSet??????=?NULL;?//處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet_WaitSetLock??=?0?;_Responsible??=?NULL?;_succ?????????=?NULL?;_cxq??????????=?NULL?;FreeNext??????=?NULL?;_EntryList????=?NULL?;?//處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表_SpinFreq?????=?0?;_SpinClock????=?0?;OwnerIsThread?=?0?;}ObjectMonitor 中有兩個(gè)隊(duì)列,_WaitSet 和 _EntryList,用來(lái)保存 ObjectWaiter 對(duì)象列表(每個(gè)等待鎖的線程都會(huì)被封裝成 ObjectWaiter 對(duì)象),_owner 指向持有 ObjectMonitor 對(duì)象的線程,當(dāng)多個(gè)線程同時(shí)訪問(wèn)一段同步代碼時(shí),首先會(huì)進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對(duì)象的 monitor 后進(jìn)入 _Owner 區(qū)域并把 monitor 中的 owner 變量設(shè)置為當(dāng)前線程同時(shí) monitor 中的計(jì)數(shù)器 count 加 1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,owner 變量恢復(fù)為 null,count 自減 1,同時(shí)該線程進(jìn)入 WaitSet 集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放 monitor(鎖) 并復(fù)位變量的值,以便其他線程進(jìn)入獲取 monitor(鎖)。
synchronized 代碼塊底層原理
首先看一下 synchronized 代碼的簡(jiǎn)單例子
public?class?SynchronizedDemo?{public?int?i?=?0;public?void?add()?{synchronized?(this)?{i++;}} }通過(guò) javap 反編譯后的結(jié)果如下,這里只貼重點(diǎn)部分
public?void?add();descriptor:?()Vflags:?ACC_PUBLICCode:stack=3,?locals=3,?args_size=10:?aload_01:?dup2:?astore_13:?monitorenter??????????????????????//?進(jìn)入同步方法4:?aload_05:?dup6:?getfield??????#2??????????????????//?Field?i:I9:?iconst_110:?iadd11:?putfield??????#2??????????????????//?Field?i:I14:?aload_115:?monitorexit???????????????????????//?退出同步方法16:?goto??????????2419:?astore_220:?aload_121:?monitorexit???????????????????????//?退出同步方法22:?aload_223:?athrow24:?return從字節(jié)碼中可知同步語(yǔ)句塊的實(shí)現(xiàn)使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開(kāi)始位置,monitorexit 指令則指明同步代碼塊的結(jié)束位置,當(dāng)執(zhí)行 monitorenter 指令時(shí),當(dāng)前線程將試圖獲取對(duì)象鎖所對(duì)應(yīng)的 monitor 的持有權(quán),當(dāng)對(duì)象鎖的 monitor 的進(jìn)入計(jì)數(shù)器為 0,那線程可以成功取得 monitor,并將計(jì)數(shù)器值設(shè)置為 1,取鎖成功。如果當(dāng)前線程已經(jīng)擁有對(duì)象鎖的 monitor 的持有權(quán),那它可以重入這個(gè) monitor (關(guān)于重入性稍后會(huì)分析),重入時(shí)計(jì)數(shù)器的值也會(huì)加 1。倘若其他線程已經(jīng)擁有對(duì)象鎖的 monitor 的所有權(quán),那當(dāng)前線程將被阻塞,直到正在執(zhí)行線程執(zhí)行完畢,即monitorexit 指令被執(zhí)行,執(zhí)行線程將釋放 monitor 并設(shè)置計(jì)數(shù)器值為0 ,其他線程將有機(jī)會(huì)持有 monitor 。值得注意的是編譯器將會(huì)確保無(wú)論方法通過(guò)何種方式完成,方法中調(diào)用過(guò)的每條 monitorenter 指令都有執(zhí)行其對(duì)應(yīng) monitorexit 指令,而無(wú)論這個(gè)方法是正常結(jié)束還是異常結(jié)束。為了保證在方法異常完成時(shí) monitorenter 和 monitorexit 指令依然可以正確配對(duì)執(zhí)行,編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理器,這個(gè)異常處理器聲明可處理所有的異常,它的目的就是用來(lái)執(zhí)行 monitorexit 指令。從字節(jié)碼中也可以看出多了一個(gè) monitorexit 指令,它就是異常結(jié)束時(shí)被執(zhí)行的釋放 monitor 的指令。
synchronized 方法的底層原理
方法級(jí)的同步是隱式,即無(wú)需通過(guò)字節(jié)碼指令來(lái)控制的,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志區(qū)分一個(gè)方法是否同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有 monitor(虛擬機(jī)規(guī)范中用的是管程一詞), 然后再執(zhí)行方法,最后再方法完成(無(wú)論是正常完成還是非正常完成)時(shí)釋放 monitor。在方法執(zhí)行期間,執(zhí)行線程持有了monitor,其他任何線程都無(wú)法再獲得同一個(gè) monitor。如果一個(gè)同步方法執(zhí)行期間拋出了異常,并且在方法內(nèi)部無(wú)法處理此異常,那這個(gè)同步方法所持有的 monitor將在異常拋到同步方法之外時(shí)自動(dòng)釋放。下面我們看看字節(jié)碼層面如何實(shí)現(xiàn):
java 代碼:
public?class?SynchronizedDemo?{public?int?i?=?0;public?synchronized?void?add()?{i++;} }通過(guò) javap 反編譯后的結(jié)果如下,這里只貼重點(diǎn)部分
public?synchronized?void?add();descriptor:?()Vflags:?ACC_PUBLIC,?ACC_SYNCHRONIZEDCode:stack=3,?locals=1,?args_size=10:?aload_01:?dup2:?getfield??????#2??????????????????//?Field?i:I5:?iconst_16:?iadd7:?putfield??????#2??????????????????//?Field?i:I10:?returnLineNumberTable:line?8:?0line?9:?10synchronized 鎖優(yōu)化
鎖的狀態(tài)總共有四種,無(wú)鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖,但是鎖的升級(jí)是單向的,也就是說(shuō)只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí),關(guān)于重量級(jí)鎖,前面我們已詳細(xì)分析過(guò),下面我們將介紹偏向鎖和輕量級(jí)鎖以及JVM的其他優(yōu)化手段。
偏向鎖
偏向鎖是 Java 6 之后加入的新鎖,它是一種針對(duì)加鎖操作的優(yōu)化手段。在大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,比如 StringBuffer 的 append 方法。因此為了減少同一線程獲取鎖的代價(jià)而引入偏向鎖。偏向鎖的核心思想是,如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無(wú)需再做任何同步操作,即獲取鎖的過(guò)程,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作,從而也就提供程序的性能。所以,對(duì)于沒(méi)有鎖競(jìng)爭(zhēng)的場(chǎng)合,偏向鎖有很好的優(yōu)化效果。需要注意的是,如果有多線程爭(zhēng)搶鎖的情況,那么偏向鎖就會(huì)失效,會(huì)升級(jí)為輕量級(jí)鎖。
偏向鎖的上鎖過(guò)程偏向鎖的獲取方式是將對(duì)象頭的?MarkWord?部分中, 標(biāo)記上線程ID, 以表示哪一個(gè)線程獲得了偏向鎖。 具體的賦值邏輯如下: 首先讀取目標(biāo)對(duì)象的 MarkWord, 判斷是否處于可偏向的狀態(tài)
如果為可偏向狀態(tài), 則嘗試用 CAS 操作, 將自己的線程 ID 寫入MarkWord
-
如果 CAS 操作成功, 則認(rèn)為已經(jīng)獲取到該對(duì)象的偏向鎖, 執(zhí)行同步塊代碼 。
-
補(bǔ)充: 一個(gè)線程在執(zhí)行完同步代碼塊以后, 并不會(huì)嘗試將 MarkWord 中的 thread ID 賦回原值 。這樣做的好處是: 如果該線程需要再次對(duì)這個(gè)對(duì)象加鎖,而這個(gè)對(duì)象之前一直沒(méi)有被其他線程嘗試獲取過(guò)鎖,依舊停留在可偏向的狀態(tài)下, 即可在不修改對(duì)象頭的情況下, 直接認(rèn)為偏向成功。
-
如果 CAS 操作失敗, 則說(shuō)明, 有另外一個(gè)線程 Thread B 搶先獲取了偏向鎖。 這種狀態(tài)說(shuō)明該對(duì)象的競(jìng)爭(zhēng)比較激烈, 此時(shí)需要撤銷 Thread B 獲得的偏向鎖,將 Thread B 持有的鎖升級(jí)為輕量級(jí)鎖。 該操作需要等待全局安全點(diǎn) JVM safepoint ( 此時(shí)間點(diǎn), 沒(méi)有線程在執(zhí)行字節(jié)碼) 。
如果是已偏向狀態(tài), 則檢測(cè) MarkWord 中存儲(chǔ)的 thread ID 是否等于當(dāng)前 thread ID 。
-
如果相等, 則證明本線程已經(jīng)獲取到偏向鎖, 可以直接繼續(xù)執(zhí)行同步代碼塊
-
如果不等, 則證明該對(duì)象目前偏向于其他線程, 需要撤銷偏向鎖
從上面的偏向鎖機(jī)制描述中,可以注意到: 偏向鎖的?撤銷(revoke)?是一個(gè)很特殊的操作, 為了執(zhí)行撤銷操作, 需要等待全局安全點(diǎn)(Safe Point), 此時(shí)間點(diǎn)所有的工作線程都停止了字節(jié)碼的執(zhí)行。
輕量級(jí)鎖
偏向鎖升級(jí)成為輕量級(jí)鎖的條件是當(dāng)有另外一個(gè)線程出現(xiàn)爭(zhēng)搶鎖的時(shí)候,會(huì)發(fā)生偏向鎖的撤銷操作。偏向鎖撤銷后, 對(duì)象可能處于兩種狀態(tài)。
-
一種是不可偏向的無(wú)鎖狀態(tài),之所以不允許偏向, 是因?yàn)橐呀?jīng)檢測(cè)到了多于一個(gè)線程的競(jìng)爭(zhēng), 升級(jí)到了輕量級(jí)鎖的機(jī)制。
-
一種是不可偏向的已鎖 ( 輕量級(jí)鎖) 狀態(tài)
之所以會(huì)出現(xiàn)上述兩種狀態(tài), 是因?yàn)槠蜴i不存在解鎖的操作, 只有撤銷操作。 觸發(fā)撤銷操作時(shí):
-
原來(lái)已經(jīng)獲取了偏向鎖的線程可能已經(jīng)執(zhí)行完了同步代碼塊, 使得對(duì)象處于 “閑置狀態(tài)”,相當(dāng)于原有的偏向鎖已經(jīng)過(guò)期無(wú)效了。此時(shí)該對(duì)象就應(yīng)該被直接轉(zhuǎn)換為不可偏向的無(wú)鎖狀態(tài)。
-
原來(lái)已經(jīng)獲取了偏向鎖的線程也可能尚未執(zhí)行完同步代碼塊, 偏向鎖依舊有效, 此時(shí)對(duì)象就應(yīng)該被轉(zhuǎn)換為被輕量級(jí)加鎖的狀態(tài)
自旋鎖
輕量級(jí)鎖失敗后,虛擬機(jī)為了避免線程真實(shí)地在操作系統(tǒng)層面掛起,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時(shí)間都不會(huì)太長(zhǎng),如果直接掛起操作系統(tǒng)層面的線程可能會(huì)得不償失,因?yàn)椴僮飨到y(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高。因此自旋鎖會(huì)假設(shè)在不久將來(lái),當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因),一般不會(huì)太久,可能是50個(gè)循環(huán)或100循環(huán),在經(jīng)過(guò)若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實(shí)也是可以提升效率的。最后沒(méi)辦法也就只能升級(jí)為重量級(jí)鎖了。
鎖消除
消除鎖是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機(jī)在JIT編譯時(shí)(可以簡(jiǎn)單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯),通過(guò)對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過(guò)這種方式消除沒(méi)有必要的鎖,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間,如下StringBuffer的append是一個(gè)同步方法,但是在add方法中的StringBuffer屬于一個(gè)局部變量,并且不會(huì)被其他線程所使用,因此StringBuffer不可能存在共享資源競(jìng)爭(zhēng)的情景,JVM會(huì)自動(dòng)將其鎖消除。
synchronized 鎖升級(jí)過(guò)程詳解
首先上圖說(shuō)明一下鎖升級(jí)的流程
膨脹過(guò)程:無(wú)鎖(鎖對(duì)象初始化時(shí))-> 偏向鎖(有線程請(qǐng)求鎖) -> 輕量級(jí)鎖(多線程輕度競(jìng)爭(zhēng))-> 重量級(jí)鎖(線程過(guò)多或長(zhǎng)耗時(shí)操作,線程自旋過(guò)度消耗cpu)
jvm默認(rèn)延時(shí)4s自動(dòng)開(kāi)啟偏向鎖(此時(shí)為匿名偏向鎖,不指向任務(wù)線程),可通過(guò) -XX:BiasedLockingStartUpDelay=0 取消延時(shí);如果不要偏向鎖,可通過(guò)-XX:-UseBiasedLocking = false來(lái)設(shè)置
鎖只能升級(jí),不能降級(jí);偏向鎖可以被重置為無(wú)鎖狀態(tài)
鎖對(duì)象頭記錄占用鎖的線程信息,但不能主動(dòng)釋放,線程棧同時(shí)記錄鎖的使用信息,當(dāng)有其他線程(T1)申請(qǐng)已經(jīng)被占用的鎖時(shí),先根據(jù)鎖對(duì)向的信息,找對(duì)應(yīng)線程棧,若線程已結(jié)束,則鎖對(duì)象先被置為無(wú)鎖狀態(tài),再被T1線程占有后置為偏向鎖;若線程位結(jié)束,則鎖狀態(tài)由當(dāng)前偏向鎖升級(jí)為輕量級(jí)鎖
偏向鎖和輕量級(jí)鎖在用戶態(tài)維護(hù),重量級(jí)鎖需要切換到內(nèi)核態(tài)(os)進(jìn)行維護(hù)
下面使用 JOL 來(lái)驗(yàn)證鎖升級(jí)的過(guò)程
無(wú)鎖
?public?static?void?printNoLock()?{Object?o?=?new?Object();System.out.println("hash:?"?+?o.hashCode());System.out.println(ClassLayout.parseInstance(o).toPrintable());}可以看到,header從高位到低位依次為:00000000 00000000 00000000 01010001 00011101 01010000 11000000 00000001,低位的最后三位是001,無(wú)鎖狀態(tài)。
匿名偏向和偏向鎖
public?static?void?printPianXiangLock()?throws?Exception?{TimeUnit.SECONDS.sleep(5);?//?等待?JVM?開(kāi)啟偏向鎖Object?o?=?new?Object();System.out.println(ClassLayout.parseInstance(o).toPrintable());synchronized?(o)?{System.out.println(ClassLayout.parseInstance(o).toPrintable());} }第一次打印為匿名偏向,第二次偏向鎖指向了main線程
輕量級(jí)鎖
public?static?void?printQingLiangLock()?throws?Exception?{TimeUnit.SECONDS.sleep(5);Object?o?=?new?Object();synchronized?(o)?{System.out.println(ClassLayout.parseInstance(o).toPrintable());}for?(int?i?=?0;?i?<?1;?i++)?{new?Thread(()?->?{print(o);}).start();} }public?static?void?print(Object?o)?{synchronized?(o)?{System.out.println(ClassLayout.parseInstance(o).toPrintable());} }下圖展示了由偏向鎖升級(jí)到輕量級(jí)鎖的過(guò)程,第一次打印的時(shí)候無(wú)多線程競(jìng)爭(zhēng),synchronized 鎖為輕量級(jí),之后由于又有新的線程爭(zhēng)搶同一把鎖,鎖狀態(tài)進(jìn)行了升級(jí),成為了輕量級(jí)鎖。
重量級(jí)鎖
public?static?void?printZhongLiangLock()?throws?Exception?{TimeUnit.SECONDS.sleep(5);Object?o?=?new?Object();synchronized?(o)?{System.out.println(ClassLayout.parseInstance(o).toPrintable());}for?(int?i?=?0;?i?<?2;?i++)?{new?Thread(()?->?{print(o);}).start();} }public?static?void?print(Object?o)?{synchronized?(o)?{System.out.println(ClassLayout.parseInstance(o).toPrintable());} }關(guān)于 synchronized 的其他關(guān)鍵點(diǎn)
synchronized 的可重入性
在 Java 中 synchronized 是基于原子性的內(nèi)部鎖機(jī)制,是可重入的,因此在一個(gè)線程調(diào)用 synchronized 方法的同時(shí)在其方法體內(nèi)部調(diào)用該對(duì)象另一個(gè) synchronized 方法,也就是說(shuō)一個(gè)線程得到一個(gè)對(duì)象鎖后再次請(qǐng)求該對(duì)象鎖,是允許的,這就是 synchronized 的可重入性,每次重入,monitor中的計(jì)數(shù)器會(huì)加 1。具體代碼如下:
/***?Synchronized?可重入性*/ public?class?ReEnterSynchronized?{public?static?void?main(String[]?args)?throws?InterruptedException?{Accounting?accounting?=?new?Accounting();Thread?t1?=?new?Thread(accounting);Thread?t2?=?new?Thread(accounting);t1.start();t2.start();t1.join();t2.join();}public?static?class?Accounting?implements?Runnable?{public?static?int?n?=?0;public?static?int?count?=?0;@Overridepublic?void?run()?{for?(int?i?=?0;?i?<?10000;?i++)?{synchronized?(this)?{n++;increase();}}}private?synchronized?void?increase()?{count++;}} }線程中斷與 synchronized
線程中斷
在 Java 中,提供了以下 3 個(gè)有關(guān)線程中斷的方法
//中斷線程(實(shí)例方法) public?void?Thread.interrupt();//判斷線程是否被中斷(實(shí)例方法) public?boolean?Thread.isInterrupted();//判斷是否被中斷并清除當(dāng)前中斷狀態(tài)(靜態(tài)方法) public?static?boolean?Thread.interrupted();當(dāng)一個(gè)線程處于被阻塞狀態(tài)或者試圖執(zhí)行一個(gè)阻塞操作時(shí),使用Thread.interrupt()方式中斷該線程,注意此時(shí)將會(huì)拋出一個(gè)InterruptedException的異常,同時(shí)中斷狀態(tài)將會(huì)被復(fù)位(由中斷狀態(tài)改為非中斷狀態(tài)),代碼演示:
public?class?InterruptThread?{public?static?void?main(String[]?args)?throws?InterruptedException?{Thread?t?=?new?Thread()?{@Overridepublic?void?run()?{try?{while?(true)?{TimeUnit.SECONDS.sleep(2);}}?catch?(InterruptedException?e)?{System.out.println("interrupted?when?sleep");boolean?interrupted?=?this.isInterrupted();System.out.println("interrupt:?"?+?interrupted);}}};t.start();TimeUnit.SECONDS.sleep(3);t.interrupt();} }輸出結(jié)果:
interrupted when sleep
interrupt: false
除了阻塞中斷的情景,我們還可能會(huì)遇到處于運(yùn)行期且非阻塞的狀態(tài)的線程,這種情況下,直接調(diào)用?Thread.interrupt()?中斷線程是不會(huì)得到任響應(yīng)的,如下代碼,將無(wú)法中斷非阻塞狀態(tài)下的線程:
/***?非阻塞情況下的線程不會(huì)被中斷,?線程會(huì)一直執(zhí)行下去*/ public?class?NoInterruptThread?{public?static?void?main(String[]?args)?throws?InterruptedException?{Thread?thread?=?new?Thread()?{@Overridepublic?void?run()?{while?(true)?{System.out.println("未被中斷");}}};thread.start();TimeUnit.SECONDS.sleep(2);thread.interrupt();} }這種情況下我們要手動(dòng)進(jìn)行中斷檢測(cè)并終止程序,修改后的代碼如下:
/***?非阻塞的線程,需要手動(dòng)進(jìn)行中斷檢測(cè),判斷線程是否被中斷*/ public?class?NoBlockThreadInterruptDemo?{public?static?void?main(String[]?args)?throws?InterruptedException?{Thread?thread?=?new?Thread()?{@Overridepublic?void?run()?{while?(true)?{if?(interrupted())?{System.out.println("線程中斷");break;}?else?{System.out.println("未被中斷");}}System.out.println("線程結(jié)束運(yùn)行");}};thread.start();TimeUnit.SECONDS.sleep(1);thread.interrupt();} }輸出結(jié)果:
未被中斷
未被中斷
未被中斷
線程中斷
線程結(jié)束運(yùn)行
?
線程中斷與 synchronized
線程的中斷操作對(duì)于正在等待獲取的鎖對(duì)象的 synchronized 方法或者代碼塊并不起作用,也就是對(duì)于synchronized 來(lái)說(shuō),如果一個(gè)線程在等待鎖,那么結(jié)果只有兩種,要么它獲得這把鎖繼續(xù)執(zhí)行,要么它就保存等待,即使調(diào)用中斷線程的方法,也不會(huì)生效。代碼演示如下:
/***?線程中斷與?Synchronized,線程在獲取?Synchronized?對(duì)象鎖的時(shí)候,是不會(huì)被中斷的*/ public?class?InterruptSynchronizedDemo?{public?static?void?main(String[]?args)?throws?InterruptedException?{InterruptSynchronized?sync?=?new?InterruptSynchronized();Thread?thread?=?new?Thread(sync);thread.start();TimeUnit.SECONDS.sleep(1);thread.interrupt();}}class?InterruptSynchronized?implements?Runnable?{public?synchronized?void?fun()?{System.out.println("call?func");while?(true)?{//?never?release?lock}}public?InterruptSynchronized()?{new?Thread()?{@Overridepublic?void?run()?{fun();}}.start();}@Overridepublic?void?run()?{while?(true)?{if?(Thread.interrupted())?{System.out.println("線程中斷");break;}?else?{fun();}}} }?
等待喚醒機(jī)制與synchronized
所謂等待喚醒機(jī)制本篇主要指的是 notify/notifyAll 和 wait 方法,在使用這 3 個(gè)方法時(shí),必須處于 synchronized 代碼塊或者 synchronized 方法中,否則就會(huì)拋出 IllegalMonitorStateException 異常,這是因?yàn)檎{(diào)用這幾個(gè)方法前必須拿到當(dāng)前對(duì)象的監(jiān)視器 monitor 對(duì)象,也就是說(shuō) notify/notifyAll 和 wait 方法依賴于 monitor 對(duì)象,在前面的分析中,我們知道 monitor 存在于對(duì)象頭的 Mark Word 中(存儲(chǔ) monitor 引用指針),而 synchronized 關(guān)鍵字可以獲取 monitor ,這也就是為什么 notify/notifyAll 和 wait 方法必須在 synchronized 代碼塊或者 synchronized 方法調(diào)用的原因。
需要特別理解的一點(diǎn)是,與 sleep 方法不同的是 wait 方法調(diào)用完成后,線程將被暫停,但 wait 方法將會(huì)釋放當(dāng)前持有的監(jiān)視器鎖(monitor),直到有線程調(diào)用 notify/notifyAll 方法后方能繼續(xù)執(zhí)行,而 sleep 方法只讓線程休眠并不釋放鎖。同時(shí) notify/notifyAll 方法調(diào)用后,并不會(huì)馬上釋放監(jiān)視器鎖,而是在相應(yīng)的 synchronized(){}/synchronized 方法執(zhí)行結(jié)束后才自動(dòng)釋放鎖。
總結(jié)
以上是生活随笔為你收集整理的深度剖析 synchronized的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: MOBIUS:百度凤巢新一代广告召回系统
- 下一篇: 图网络中的社群及社群发现算法