前言
????Synchronized原理是面試中的一個難點。網(wǎng)上的各種資料太亂了 ,概念晦澀難懂,看了不少資料、博客,花了不少時間,才整理成這篇筆記??赐陮δ愦笥袔椭?/p>
1、內(nèi)存布局
????要想了解Synchronized的原理,你先必須了解下Java對象內(nèi)存布局。
????我這里就先介紹下Java內(nèi)存布局。
????當(dāng)你通過關(guān)鍵字new關(guān)鍵字創(chuàng)建一個類的實例對象,對象存于內(nèi)存的堆中,并給其分配一個內(nèi)存地址,那么是否想過如下這些問題:
- 這個實例對象是以怎樣的形態(tài)存在內(nèi)存中的?
- 一個Object對象在內(nèi)存中占用多大?
- 對象中的屬性是如何在內(nèi)存中分配的?
ps:創(chuàng)建一個對象的方式有很多種。你可以想想有哪些哦!
????Java對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充。如下圖:

實例變量
????即實例數(shù)據(jù)。存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息。
- 如果對象有屬性字段,則這里會有數(shù)據(jù)信息。如果對象無屬性字段,則這里就不會有數(shù)據(jù)。
- 根據(jù)字段類型的不同占不同的字節(jié)。例如boolean類型占1個字節(jié),int類型占4個字節(jié)等等。這部分內(nèi)存按4字節(jié)對齊。
這部分的存儲順序會受到虛擬機分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。
HotSpot虛擬機 默認(rèn)的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)。
從分配策略中可以看出,相同寬度的字段總是被分配到一起。
在滿足這個前提條件的情況下,在父類中定義的變量會出現(xiàn)在子類之前。如果 CompactFields參數(shù)值為true(默認(rèn)為true),那子類之中較窄的變量也可能會插入到父類變量的空隙之中。
填充數(shù)據(jù)
????填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊。
????由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說,就是對象的大小必須是8字節(jié)的整數(shù)倍。而對象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對象實例數(shù)據(jù)部分沒有對齊時,就需要通過對齊填充來補全。
????為什么要對齊數(shù)據(jù)?
????字段內(nèi)存對齊的其中一個原因,是讓字段只出現(xiàn)在同一CPU的緩存行中。
????如果字段不是對齊的,那么就有可能出現(xiàn)跨緩存行的字段。也就是說,該字段的讀取可能需要替換兩個緩存行,而該字段的存儲也會同時污染兩個緩存行。這兩種情況對程序的執(zhí)行效率而言都是不利的。其實對其填充的最終目的是為了計算機高效尋址。
對象頭
????對象頭是實現(xiàn)synchronized的鎖對象的基礎(chǔ),我們重點分析下。
????我們可以在Hotspot 官方文檔中找到它的描述(如下):
object header
Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.
????從中可以發(fā)現(xiàn),它是Java對象和虛擬機內(nèi)部對象都有的共同格式,由兩個字(計算機術(shù)語)組成。另外,如果對象是一個Java數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因為虛擬機可以通過普通Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是從數(shù)組的元數(shù)據(jù)中無法確定數(shù)組的大小。
????它里面提到了對象頭由兩個字組成,這兩個字是什么呢?我們還是在上面的那個Hotspot官方文檔中往上看,可以發(fā)現(xiàn)還有另外兩個名詞的定義解釋,分別是 mark word 和 klass pointer:
klass pointer
The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the "klass" contains a C++ style "vtable".
mark word
The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.
????從中可以發(fā)現(xiàn)對象頭中那兩個字:第一個字就是 mark word,第二個就是 klass pointer。
Mark Word
????即標(biāo)記字段。用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等等。
????Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit。我們打開openjdk的源碼包,對應(yīng)路徑/openjdk/hotspot/src/share/vm/oops,Mark Word對應(yīng)到C++的代碼markOop.hpp,可以從注釋中看到它們的組成,本文所有代碼是基于Jdk1.8。
需要源碼的同學(xué)請留言
????由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關(guān)系的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設(shè)計成為一個非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲更多有效的數(shù)據(jù),它會根據(jù)對象本身的狀態(tài)復(fù)用自己的存儲空間。
????Mark Word在不同的鎖狀態(tài)下存儲的內(nèi)容不同,在32位JVM中是這么存的:

????在64位JVM中是這么存的:

????雖然它們在不同位數(shù)的JVM中長度不一樣,但是基本組成內(nèi)容是一致的。
- 鎖標(biāo)志位(lock):區(qū)分鎖狀態(tài),11時表示對象待GC回收狀態(tài), 只有最后2位鎖標(biāo)識(11)有效。
- biased_lock:是否偏向鎖,由于正常鎖和偏向鎖的鎖標(biāo)識都是 01,沒辦法區(qū)分,這里引入一位的偏向鎖標(biāo)識位。
- 分代年齡(age):表示對象被GC的次數(shù),當(dāng)該次數(shù)到達(dá)閾值的時候,對象就會轉(zhuǎn)移到老年代。
- 對象的hashcode(hash):運行期間調(diào)用System.identityHashCode()來計算,延遲計算,并把結(jié)果賦值到這里。當(dāng)對象加鎖后,計算的結(jié)果31位不夠表示,在偏向鎖,輕量鎖,重量鎖,hashcode會被轉(zhuǎn)移到Monitor中。
- 偏向鎖的線程ID(JavaThread):偏向模式的時候,當(dāng)某個線程持有對象的時候,對象這里就會被置為該線程的ID。 在后面的操作中,就無需再進(jìn)行嘗試獲取鎖的動作。
- epoch:偏向鎖在CAS鎖操作過程中,偏向性標(biāo)識,表示對象更偏向哪個鎖。
- ptr_to_lock_record:輕量級鎖狀態(tài)下,指向棧中鎖記錄的指針。當(dāng)鎖獲取是無競爭的時,JVM使用原子操作而不是OS互斥。這種技術(shù)稱為輕量級鎖定。在輕量級鎖定的情況下,JVM通過CAS操作在對象的標(biāo)題字中設(shè)置指向鎖記錄的指針。
- ptr_to_heavyweight_monitor:重量級鎖狀態(tài)下,指向?qū)ο蟊O(jiān)視器Monitor的指針。如果兩個不同的線程同時在同一個對象上競爭,則必須將輕量級鎖定升級到Monitor以管理等待的線程。在重量級鎖定的情況下,JVM在對象的ptr_to_heavyweight_monitor設(shè)置指向Monitor的指針。
Klass Pointer
????即類型指針,是對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
數(shù)組長度(只有數(shù)組對象有)
????如果對象是一個數(shù)組,那在對象頭中還必須有一塊數(shù)據(jù)用于記錄數(shù)組長度。
????因為虛擬機可以通過普通Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是從數(shù)組的元數(shù)據(jù)中無法確定數(shù)組的大小。
????至此,我們已經(jīng)了解了對象在堆內(nèi)存中的整體結(jié)構(gòu)布局,如下圖所示:

2、Synchronized底層實現(xiàn)
????這里我們主要分析一下synchronized對象鎖(也就是重量級鎖)。
????在32位和64位機器上鎖標(biāo)識位都為10,其中指針指向的是monitor對象(也稱為管程或監(jiān)視器鎖)的起始地址。
????每個對象都存在著一個 monitor 與之關(guān)聯(lián),對象與其 monitor 之間的關(guān)系有存在多種實現(xiàn)方式,如:monitor可以與對象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對象鎖時自動生成,但當(dāng)一個 monitor 被某個線程持有后,它便處于鎖定狀態(tài)。
????在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現(xiàn))
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數(shù)
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //處于wait狀態(tài)的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
我們分析下上面源碼中幾個關(guān)鍵屬性:
- _WaitSet和_EntryList:用來保存ObjectWaiter對象列表(ObjectWaiter對象:每個等待鎖的線程都會被封裝成ObjectWaiter對象)。
???????有沒有發(fā)現(xiàn),他們一個是是set,一個是list,我們知道list是有序的,set無需,這保證了_EntryList中的線程有先后獲取鎖的特性,而_WaitSet中的線程不能提供這個保證。這也真是notify/notifyall 在喚醒的時候,只能隨機喚醒一個線程的緣故。(此處只是個人猜想)
- _owner:指向持有ObjectMonitor對象的線程。
????當(dāng)多個線程同時訪問一段同步代碼時,首先會進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對象的monitor 后進(jìn)入 _Owner 區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時monitor中的計數(shù)器count加1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時該線程進(jìn)入 WaitSet集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。如下圖所示:

????由此看來,monitor對象存在于每個Java對象的對象頭中(存儲的是指針),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因。
????下面我們將進(jìn)一步分析synchronized在字節(jié)碼層面的具體語義實現(xiàn)。
3、synchronized修飾代碼塊底層原理
????現(xiàn)在我們重新定義一個synchronized修飾的同步代碼塊(i++),在代碼塊中操作共享變量i,如下:
public class TestSafeAddI {
public int i;
public void addI() {
synchronized (this) {
i++;
}
}
}
????使用反編譯工具,查看編譯后的字節(jié)碼(完整):
如何查看字節(jié)碼文件,有多種工具,我這里提供2種:
方式一:luyten工具
運行工具,然后Settings選擇ByteCode,然后導(dǎo)入本地的.class文件即可。
需要該工具的同學(xué),請留言!??!
方式二:使用idea編輯器的同學(xué),可以在idea中選中編譯后的.class文件,然后View->Show ByteCode
ps:本人使用的是idea2020最新版本。
class com.top.test.mutiTheread.TestSafeAddI
Minor version: 0
Major version: 52
Flags: PUBLIC, SUPER
public int i;
Flags: PUBLIC
public void <init>();
Flags: PUBLIC
Code:
linenumber 3
0: aload_0 /* this */
1: invokespecial java/lang/Object.<init>:()V
4: return
public void addI();
Flags: PUBLIC
Code:
linenumber 7
0: aload_0 /* this */
1: dup
2: astore_1
3: monitorenter
linenumber 8
4: aload_0 /* this */
5: dup
6: getfield com/top/test/mutiTheread/TestSafeAddI.i:I
9: iconst_1
10: iadd
11: putfield com/top/test/mutiTheread/TestSafeAddI.i:I
linenumber 9
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
linenumber 10
24: return
StackMapTable: 00 02 FF 00 13 00 02 07 00 10 07 00 11 00 01 07 00 12 FA 00 04
Exceptions:
Try Handler
Start End Start End Type
----- ----- ----- ----- ----
4 16 19 24 Any
19 22 19 24 Any
????我們主要關(guān)注字節(jié)碼中的如下代碼:
3: monitorenter //進(jìn)入同步方法
//..........省略其他
15: monitorexit //退出同步方法
16: goto 24
//省略其他.......
21: monitorexit //退出同步方法
????從字節(jié)碼中可知同步語句塊的實現(xiàn)使用的是monitorenter和monitorexi指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結(jié)束位置。
當(dāng)執(zhí)行monitorenter指令時:
當(dāng)前線程將試圖獲取 objectref(即對象鎖) 所對應(yīng)的 monitor 的持有權(quán),當(dāng) objectref 的 monitor 的進(jìn)入計數(shù)器為 0,那線程可以成功取得 monitor,并將計數(shù)器值設(shè)置為 1,取鎖成功。
如果當(dāng)前線程已經(jīng)擁有 objectref 的 monitor 的持有權(quán),那它可以重入這個 monitor,重入時計數(shù)器的值也會加 1。這正是synchronized的可重入特性。(關(guān)于可重入鎖可以看這篇:可重入鎖-面試題:synchronized是可重入鎖嗎?)
倘若其他線程已經(jīng)擁有 objectref 的 monitor 的所有權(quán),即目標(biāo)鎖對象的計數(shù)器不為0。那當(dāng)前線程將被阻塞,直到正在執(zhí)行線程執(zhí)行完畢.
當(dāng)執(zhí)行 monitorexit 時:
- Java 虛擬機則需將鎖對象的計數(shù)器減 1。當(dāng)計數(shù)器減為 0 時,那便代表該鎖已經(jīng)被釋放掉了。這樣其他線程將有機會持有 monitor 。
- 計數(shù)器不為0,表示當(dāng)前線程還持有該對象鎖。
????值得注意的是:一條指令Monitorenter可以對應(yīng)到多條monitorexit 指令。這是因為 Java 虛擬機需要確保所獲得的鎖在正常執(zhí)行路徑,以及異常執(zhí)行路徑上都能夠被解鎖。????也就是說:編譯器將會確保無論方法通過何種方式完成,方法中調(diào)用過的每條 monitorenter 指令都有執(zhí)行其對應(yīng) monitorexit 指令,而無論這個方法是正常結(jié)束還是異常結(jié)束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執(zhí)行,編譯器會自動產(chǎn)生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行 monitorexit 指令。從字節(jié)碼中也可以看出多了一個monitorexit指令,它就是異常結(jié)束時被執(zhí)行的釋放monitor 的指令。
4、synchronized修飾方法底層原理
????synchronized修飾方法與修飾代碼塊有不同。
????我們把上面的同步方法改下 ,改成synchronized修飾方法:
public class TestSafeAddI {
public int i;
public synchronized void addI() {
i++;
}
}
????反編譯后的字節(jié)碼如下:
class com.top.test.mutiTheread.TestSafeAddI
Minor version: 0
Major version: 52
Flags: PUBLIC, SUPER
public int i;
Flags: PUBLIC
public void <init>();
Flags: PUBLIC
Code:
linenumber 3
0: aload_0 /* this */
1: invokespecial java/lang/Object.<init>:()V
4: return
public synchronized void addI();
Flags: PUBLIC, SYNCHRONIZED
Code:
linenumber 7
0: aload_0 /* this */
1: dup
2: getfield com/top/test/mutiTheread/TestSafeAddI.i:I
5: iconst_1
6: iadd
7: putfield com/top/test/mutiTheread/TestSafeAddI.i:I
linenumber 8
10: return
????當(dāng)用synchronized 標(biāo)記方法時,并沒有monitorenter指令和monitorexit指令,從字節(jié)碼中,我們可以看到方法的訪問標(biāo)記包括ACC_SYNCHRONIZED了。該標(biāo)識指明了該方法是一個同步方法,JVM通過該ACC_SYNCHRONIZED訪問標(biāo)志來辨別一個方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。在進(jìn)入該方法時,Java 虛擬機需要進(jìn)行 monitorenter操作。而在退出該方法時,不管是正常返回,還是向調(diào)用者拋異常,Java 虛擬機均需要進(jìn)行monitorexit操作。
????這里 monitorenter 和 monitorexit 操作所對應(yīng)的鎖對象是隱式的。對于實例方法來說,這兩個操作對應(yīng)的鎖對象是 this;對于靜態(tài)方法來說,這兩個操作對應(yīng)的鎖對象則是所在類的 Class 實例。
????同時我們還必須注意到的是在Java早期版本中,synchronized屬于重量級鎖,效率低下。因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)。這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高,這也是為什么早期的synchronized效率低的原因。
5、鎖的升級
????鎖的升級,我們可以理解為:Java虛擬機對synchronized的優(yōu)化
????為了盡量避免昂貴的線程阻塞、喚醒操作,Java 虛擬機會在線程進(jìn)入阻塞狀態(tài)之前,以及被喚醒后競爭不到鎖的情況下,進(jìn)入自旋狀態(tài),在處理器上空跑并且輪詢鎖是否被釋放。如果此時鎖恰好被釋放了,那么當(dāng)前線程便無須進(jìn)入阻塞狀態(tài),而是直接獲得這把鎖。我們稱其為自旋鎖。
????同時在Java6之后Java官方對從JVM層面對synchronized較大優(yōu)化,所以現(xiàn)在的synchronized鎖效率也優(yōu)化得很不錯了,Java 6之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖(也叫:偏斜鎖,英文單詞為,Biased Locking)。
????鎖的升級:鎖的狀態(tài)總共有四種(上面的Mark Word圖結(jié)構(gòu)也可以看出),無鎖狀態(tài)、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖。。
ps:有的觀點認(rèn)為 Java 不會進(jìn)行鎖降級。實際上,鎖降級確實是會發(fā)生的,當(dāng) JVM 進(jìn)入安全點(SafePoint)的時候,會檢查是否有閑置的 Monitor,然后試圖進(jìn)行降級。
????關(guān)于重量級鎖,前面我們已詳細(xì)分析過。下面我們將介紹偏向鎖、輕量級鎖、自旋鎖以及JVM的其他優(yōu)化手段。
6、偏向鎖
????偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優(yōu)化手段。
????偏向鎖是最樂觀的一種情況:在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。
因此為了減少同一線程獲取鎖的代價而引入偏向鎖。
????偏向鎖的核心思想是:如果一個線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)這個線程再次請求鎖時,無需再做任何同步操作,直接可以獲取鎖。這樣就省去了大量有關(guān)鎖申請的操作,從而也就提供程序的性能。
????加鎖時,如果該鎖對象支持偏向鎖,那么 Java 虛擬機會通過CAS操作,將當(dāng)前線程的地址(我理解的是線程ID,不過都能確定唯一線程)記錄在鎖對象的標(biāo)記字段之中,并且將標(biāo)記字段的最后三位設(shè)置為101。(便于理解,我把Mark Word的結(jié)構(gòu)圖再放在這里)
CAS 是一個原子操作,它會比較目標(biāo)地址的值是否和期望值相等,如果相等,則替換為一個新的值。

????在接下來的運行過程中,每當(dāng)有線程請求這把鎖,Java 虛擬機只需判斷鎖對象標(biāo)記字段中:最后三位是否為 101,是否包含當(dāng)前線程的地址,以及epoch值是否和鎖對象的類的 epoch 值相同。如果都滿足,那么當(dāng)前線程持有該偏向鎖,可以直接返回。
理解epoch值:
????我們先從偏向鎖的撤銷講起。當(dāng)請求加鎖的線程和鎖對象標(biāo)記字段的線程地址不匹配時(而且 epoch 值相等,如若不等,那么當(dāng)前線程可以將該鎖重偏向至自己),Java 虛擬機需要撤銷該偏向鎖。這個撤銷過程非常麻煩,它要求持有偏向鎖的線程到達(dá)安全點,再將偏向鎖替換成輕量級鎖。
????如果某一類鎖對象的總撤銷數(shù)超過了一個閾值(對應(yīng) Java 虛擬機參數(shù) -XX:BiasedLockingBulkRebiasThreshold,默認(rèn)為 20),那么 Java 虛擬機會宣布這個類的偏向鎖失效。
????具體的做法便是在每個類中維護一個 epoch 值,你可以理解為第幾代偏向鎖。當(dāng)設(shè)置偏向鎖時,Java 虛擬機需要將該 epoch 值復(fù)制到鎖對象的標(biāo)記字段中。
????在宣布某個類的偏向鎖失效時,Java 虛擬機實則將該類的 epoch 值加 1,表示之前那一代的偏向鎖已經(jīng)失效。而新設(shè)置的偏向鎖則需要復(fù)制新的 epoch 值。
????為了保證當(dāng)前持有偏向鎖并且已加鎖的線程不至于因此丟鎖,Java 虛擬機需要遍歷所有線程的 Java 棧,找出該類已加鎖的實例,并且將它們標(biāo)記字段中的 epoch 值加 1。該操作需要所有線程處于安全點狀態(tài)。
????如果總撤銷數(shù)超過另一個閾值(對應(yīng) Java 虛擬機參數(shù) -XX:BiasedLockingBulkRevokeThreshold,默認(rèn)值為 40),那么 Java 虛擬機會認(rèn)為這個類已經(jīng)不再適合偏向鎖。此時,Java 虛擬機會撤銷該類實例的偏向鎖,并且在之后的加鎖過程中直接為該類實例設(shè)置輕量級鎖。
7、輕量級鎖
????倘若偏向鎖失敗,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖
????輕量級鎖時Java6引入的。
????輕量級鎖是一種比較樂觀的情況:多個線程在不同的時間段請求同一把鎖,也就是說沒有鎖競爭。
????標(biāo)記字段(mark word)的最后兩位被用來表示該對象的鎖狀態(tài)。其中,00 代表輕量級鎖,01 代表無鎖(或偏向鎖),10 代表重量級鎖。
????當(dāng)進(jìn)行加鎖操作時,Java 虛擬機會判斷是否已經(jīng)是重量級鎖。如果不是,它會在當(dāng)前線程的當(dāng)前棧楨中劃出一塊空間,作為該鎖的鎖記錄,并且將鎖對象的標(biāo)記字段 復(fù)制到該鎖記錄中(可以理解為保存之前鎖對象的標(biāo)記字段。如果是同一個線程這個值會是0:后面的鎖記錄清零就是這個意思)。
????然后,Java 虛擬機會嘗試用 CAS(compare-and-swap)操作替換鎖對象的標(biāo)記字段。
????假設(shè)當(dāng)前鎖對象的標(biāo)記字段為 X…XYZ,Java 虛擬機會比較該字段是否為 X…X01(鎖標(biāo)志位01表示偏向鎖)。如果是,則替換為剛才分配的鎖記錄的地址。由于內(nèi)存對齊的緣故,它的最后兩位為 00(鎖標(biāo)志位00表示輕量級鎖)。此時,該線程已成功獲得這把鎖,可以繼續(xù)執(zhí)行了。
????如果不是 X…X01,那么有兩種可能。第一,該線程重復(fù)獲取同一把鎖(此刻持有的是輕量級鎖)。此時,Java 虛擬機會將鎖記錄清零,以代表該鎖被重復(fù)獲取(可重入鎖可以閱讀下:)。第二,其他線程持有該鎖(此刻持有的是輕量級鎖)。此時,Java 虛擬機會將這把鎖膨脹為重量級鎖,并且阻塞當(dāng)前線程。
????當(dāng)進(jìn)行解鎖操作時,如果當(dāng)前鎖記錄(你可以將一個線程的所有鎖記錄想象成一個棧結(jié)構(gòu),每次加鎖壓入一條鎖記錄,解鎖彈出一條鎖記錄,當(dāng)前鎖記錄指的便是棧頂?shù)逆i記錄)的值為 0,則代表重復(fù)進(jìn)入同一把鎖,直接返回即可。
????否則,Java 虛擬機會嘗試用 CAS 操作,比較鎖對象的標(biāo)記字段的值是否為當(dāng)前鎖記錄的地址。如果是,則替換為鎖記錄中的值,也就是鎖對象原本的標(biāo)記字段。此時,該線程已經(jīng)成功釋放這把鎖。
????如果不是,則意味著這把鎖已經(jīng)被膨脹為重量級鎖。此時,Java 虛擬機會進(jìn)入重量級鎖的釋放過程,喚醒因競爭該鎖而被阻塞了的線程。
8、自旋鎖
????輕量級鎖失敗后,虛擬機為了避免線程真實地在操作系統(tǒng)層面掛起,還會進(jìn)行一項稱為自旋鎖的優(yōu)化手段。
????這是基于在大多數(shù)情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統(tǒng)層面的線程可能會得不償失,畢竟操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高。
????因此自旋鎖會假設(shè)在不久將來,當(dāng)前的線程可以獲得鎖,因此虛擬機會讓當(dāng)前想要獲取鎖的線程做幾個空循環(huán)(這也是稱為自旋的原因),一般不會太久,可能是50個循環(huán)或100循環(huán),在經(jīng)過若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會將線程在操作系統(tǒng)層面掛起。
????這就是自旋鎖的優(yōu)化方式,這種方式確實也是可以提升效率的。最后沒辦法也就只能升級為重量級鎖了。
舉個例子:
????我們可以用等紅綠燈作為例子。Java 線程的阻塞相當(dāng)于熄火停車,而自旋狀態(tài)相當(dāng)于怠速停車。如果紅燈的等待時間非常長,那么熄火停車相對省油一些;如果紅燈的等待時間非常短,比如說我們在 synchronized 代碼塊里只做了一個整型加法,那么在短時間內(nèi)鎖肯定會被釋放出來,因此怠速停車更加合適。
????然而,對于 Java 虛擬機來說,它并不能看到紅燈的剩余時間,也就沒辦法根據(jù)等待時間的長短來選擇自旋還是阻塞。Java 虛擬機給出的方案是自適應(yīng)自旋,根據(jù)以往自旋等待時是否能夠獲得鎖,來動態(tài)調(diào)整自旋的時間(循環(huán)數(shù)目)。
就我們的例子來說,如果之前不熄火等到了綠燈,那么這次不熄火的時間就長一點;如果之前不熄火沒等到綠燈,那么這次不熄火的時間就短一點。
????自旋狀態(tài)還帶來另外一個副作用,那便是不公平的鎖機制。處于阻塞狀態(tài)的線程,并沒有辦法立刻競爭被釋放的鎖。然而,處于自旋狀態(tài)的線程,則很有可能優(yōu)先獲得這把鎖。(關(guān)于公平鎖與非公平鎖可以看這篇:公平鎖和非公平鎖-ReentrantLock是如何實現(xiàn)公平、非公平的)
9、鎖消除
????消除鎖是虛擬機另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當(dāng)某段代碼即將第一次被執(zhí)行時進(jìn)行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請求鎖時間。
????如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量,并且不會被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。
public class StringBufferRemoveSync {
public void add(String str1, String str2) {
//StringBuffer是線程安全,由于sb只會在append方法中使用,不可能被其他線程引用
//因此sb屬于不可能共享的資源,JVM會自動消除內(nèi)部的鎖
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
for (int i = 0; i < 10000000; i++) {
rmsync.add("abc", "123");
}
}
}
總結(jié)
????我整理的還不夠完善,比如:內(nèi)存布局的壓縮指針和字段重排列我都沒有提及。
????不足之處,有疑問的同學(xué)可以留言討論哦。
????如果覺得有所收獲的話,不妨給個小心心吧?。?!
推薦閱讀:
Java內(nèi)存模型-volatile的應(yīng)用(實例講解)
synchronized解決原子性-synchronized的三種應(yīng)用方式(實例講解)
線程池-一文弄懂Java里面的線程池ThreadPoolExecutor
可重入鎖-面試題:synchronized是可重入鎖嗎