上一篇文章介紹了多線程的概念及synchronized
的使用方法《synchronized的使用(一)》,可是僅僅會用仍是不夠的,只有瞭解其底層實現才能在開發過程當中指揮若定,因此本篇探討synchronized
的實現原理及鎖升級(膨脹)的過程。html
synchronized
是依賴於JVM
來實現同步的,在同步方法和代碼塊的原理有點區別。java
咱們在代碼塊加上synchronized
關鍵字數組
public void synSay() {
synchronized (object) {
System.out.println("synSay----" + Thread.currentThread().getName());
}
}
複製代碼
編譯以後,咱們利用反編譯命令javap -v xxx.class
查看對應的字節碼,這裏爲了減小篇幅,我就只粘貼對應的方法的字節碼。安全
public void synSay();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: getfield #2 // Field object:Ljava/lang/String;
4: dup
5: astore_1
6: monitorenter
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: new #4 // class java/lang/StringBuilder
13: dup
14: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
17: ldc #6 // String synSay----
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokestatic #8 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
25: invokevirtual #9 // Method java/lang/Thread.getName:()Ljava/lang/String;
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: aload_1
38: monitorexit
39: goto 47
42: astore_2
43: aload_1
44: monitorexit
45: aload_2
46: athrow
47: return
Exception table:
from to target type
7 39 42 any
42 45 42 any
LineNumberTable:
line 21: 0
line 22: 7
line 23: 37
line 24: 47
LocalVariableTable:
Start Length Slot Name Signature
0 48 0 this Lcn/T1;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 42
locals = [ class cn/T1, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
複製代碼
能夠發現synchronized
同步代碼塊是經過加monitorenter
和monitorexit
指令實現的。
每一個對象都有個**監視器鎖(monitor) **,當monitor
被佔用的時候就表明對象處於鎖定狀態,而monitorenter
指令的做用就是獲取monitor
的全部權,monitorexit
的做用是釋放monitor
的全部權,這二者的工做流程以下:
monitorenter:bash
monitor
的進入數爲0,則線程進入到monitor
,而後將進入數設置爲1
,該線程稱爲monitor
的全部者。monitor
(即monitor
進入數不爲0),而後該線程又從新進入monitor
,則將monitor
的進入數+1
,這個即爲鎖的重入。monitor
,則該線程進入到阻塞狀態,知道monitor
的進入數爲0,該線程再去從新嘗試獲取monitor
的全部權。monitorexit:執行該指令的線程必須是monitor
的全部者,指令執行時,monitor
進入數-1
,若是-1
後進入數爲0
,那麼線程退出monitor
,再也不是這個monitor
的全部者。這個時候其它阻塞的線程能夠嘗試獲取monitor
的全部權。多線程
在方法上加上synchronized
關鍵字併發
synchronized public void synSay() {
System.out.println("synSay----" + Thread.currentThread().getName());
}
複製代碼
編譯以後,咱們利用反編譯命令javap -v xxx.class
查看對應的字節碼,這裏爲了減小篇幅,我就只粘貼對應的方法的字節碼。app
public synchronized void synSay();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String synSay----
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokestatic #7 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
18: invokevirtual #8 // Method java/lang/Thread.getName:()Ljava/lang/String;
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
LineNumberTable:
line 20: 0
line 21: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this Lcn/T1;
複製代碼
從字節碼上看,加有synchronized
關鍵字的方法,常量池中比普通的方法多了個ACC_SYNCHRONIZED
標識,JVM
就是根據這個標識來實現方法的同步。
當調用方法的時候,調用指令會檢查方法是否有ACC_SYNCHRONIZED
標識,有的話線程須要先獲取monitor
,獲取成功才能繼續執行方法,方法執行完畢以後,線程再釋放monitor
,同一個monitor
同一時刻只能被一個線程擁有。佈局
synchronized
同步代碼塊的時候經過加入字節碼monitorenter
和monitorexit
指令來實現monitor
的獲取和釋放,也就是須要JVM經過字節碼顯式的去獲取和釋放monitor實現同步,而synchronized同步方法的時候,沒有使用這兩個指令,而是檢查方法的ACC_SYNCHRONIZED
標誌是否被設置,若是設置了則線程須要先去獲取monitor,執行完畢了線程再釋放monitor,也就是不須要JVM去顯式的實現。
這兩個同步方式實際都是經過獲取monitor和釋放monitor來實現同步的,而monitor的實現依賴於底層操做系統的mutex
互斥原語,而操做系統實現線程之間的切換的時候須要從用戶態轉到內核態,這個轉成過程開銷比較大。
線程獲取、釋放monitor
的過程以下:post
線程嘗試獲取monitor
的全部權,若是獲取失敗說明monitor
被其餘線程佔用,則將線程加入到的同步隊列中,等待其餘線程釋放monitor
,當其餘線程釋放monitor
後,有可能恰好有線程來獲取monitor
的全部權,那麼系統會將monitor
的全部權給這個線程,而不會去喚醒同步隊列的第一個節點去獲取,因此synchronized
是非公平鎖。若是線程獲取monitor
成功則進入到monitor
中,而且將其進入數+1
。
關於什麼是公平鎖、非公平鎖能夠參考一下美團技術團隊寫的《不可不說的Java「鎖」事》
到這裏咱們也清楚了synchronized
的語義底層是經過一個monitor
的對象完成,其實wait
、notiyf
和notifyAll
等方法也是依賴於monitor
對象來完成的,這也就是爲何須要在同步方法或者同步代碼塊中調用的緣由(須要先獲取對象的鎖,才能執行),不然會拋出java.lang.IllegalMonitorStateException
的異常
咱們知道了線程要訪問同步方法、代碼塊的時候,首先須要取得鎖,在退出或者拋出異常的時候又必須釋放鎖,那麼鎖究竟是什麼?又儲存在哪裏?
爲了解開這個疑問,咱們須要進入Java虛擬機(JVM) 的世界。在HotSpot
虛擬機中,Java
對象在內存中儲存的佈局能夠分爲3
塊區域:對象頭、實例數據、對齊填充。synchronized使用的鎖對象儲存在對象頭中
對象頭的數據長度在32
位和64
位(未開啓壓縮指針)的虛擬機中分別爲32bit
和64bit
。對象頭由如下三個部分組成:
GC
分代年齡、鎖標誌位、線程持有的鎖、偏向線程ID
、偏向時間戳、對象分代年齡等。注意這個Mark Word結構並非固定的,它會隨着鎖狀態標誌的變化而變化,並且裏面的數據也會隨着鎖狀態標誌的變化而變化,這樣作的目的是爲了節省空間。在32
位虛擬機下,Mark Word
的結構和數據可能爲如下5
種中的一種。
在64
位虛擬機下,Mark Word
的結構和數據可能爲如下2
種中的一種。
這裏重點注意是否偏向鎖和鎖標誌位,這兩個標識和synchronized
的鎖膨脹息息相關。
儲存着對象的實際數據,也就是咱們在程序中定義的各類類型的字段內容。
HotSpot
虛擬機的對齊方式爲8
字節對齊,即一個對象必須爲8
字節的整數倍,若是不是,則經過這個對齊填充來佔位填充。
上文介紹的 "synchronized
實現原理" 實際是synchronized實現重量級鎖的原理,那麼上文頻繁提到monitor
對象和對象又存在什麼關係呢,或者說monitor
對象儲存在對象的哪一個地方呢?
在對象的對象頭中,當鎖的狀態爲重量級鎖的時候,它的指針即指向monitor
對象,如圖:
monitor
對象?答案:是的。
JVM
對
synchronized
的優化,咱們知道重量級鎖的實現是基於底層操做系統的
mutex
互斥原語的,這個開銷是很大的。因此
JVM
對
synchronized
作了優化,
JVM
先利用對象頭實現鎖的功能,若是線程的競爭過大則會將鎖升級(膨脹)爲重量級鎖,也就是使用
monitor
對象。固然
JVM
對鎖的優化不只僅只有這個,還有引入適應性自旋、鎖消除、鎖粗化、輕量級鎖、偏向鎖等。
那麼鎖的是怎麼進行膨脹的或者依據什麼來膨脹,這也就是本篇須要介紹的重點,首先咱們須要瞭解幾個概念。
自旋:當有個線程A
去請求某個鎖的時候,這個鎖正在被其它線程佔用,可是線程A
並不會立刻進入阻塞狀態,而是循環請求鎖(自旋)。這樣作的目的是由於不少時候持有鎖的線程會很快釋放鎖的,線程A
能夠嘗試一直請求鎖,不必被掛起放棄CPU
時間片,由於線程被掛起而後到喚醒這個過程開銷很大,固然若是線程A
自旋指定的時間尚未得到鎖,仍然會被掛起。
自適應性自旋:自適應性自旋是自旋的升級、優化,自旋的時間再也不固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態決定。例如線程若是自旋成功了,那麼下次自旋的次數會增多,由於JVM
認爲既然上次成功了,那麼此次自旋也頗有可能成功,那麼它會容許自旋的次數更多。反之,若是對於某個鎖,自旋不多成功,那麼在之後獲取這個鎖的時候,自旋的次數會變少甚至忽略,避免浪費處理器資源。有了自適應性自旋,隨着程序運行和性能監控信息的不斷完善,JVM
對程序鎖的情況預測就會變得愈來愈準確,JVM
也就變得愈來愈聰明。
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。
在使用鎖的時候,須要讓同步塊的做用範圍儘量小,這樣作的目的是爲了使須要同步的操做數量儘量小,若是存在鎖競爭,那麼等待鎖的線程也能儘快拿到鎖。
所謂輕量級鎖是相對於使用底層操做系統mutex
互斥原語實現同步的重量級鎖而言的,由於輕量級鎖同步的實現是基於對象頭的Mark Word。那麼輕量級鎖是怎麼使用對象頭來實現同步的呢,咱們看看具體實現過程。
獲取鎖過程:
Mark Word
拷貝到線程的鎖記錄(Lock Recored)中。CAS
操做嘗試將對象的Mark Word
更新爲指向Lock Record
的指針。若是這個更新成功了,則執行步驟4
,不然執行步驟5
。Mark Word
是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,能夠直接進入同步塊繼續執行,不然說明這個鎖對象已經被其其它線程搶佔了。進行自旋執行步驟3
,若是自旋結束仍然沒有得到鎖,輕量級鎖就須要膨脹爲重量級鎖,鎖標誌位狀態值變爲"10",Mark Word中儲存就是指向monitor
對象的指針,當前線程以及後面等待鎖的線程也要進入阻塞狀態。釋放鎖的過程:
CAS
操做將對象當前的Mark Word
和線程中複製的Displaced Mark Word
替換回來(依據Mark Word
中鎖記錄指針是否還指向本線程的鎖記錄),若是替換成功,則執行步驟2
,不然執行步驟3
。偏向鎖的目的是消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能。若是說輕量級鎖是在無競爭的狀況下使用CAS
操做區消除同步使用的互斥量,那麼偏向鎖就是在無競爭的狀況下把整個同步都消除掉,連CAS
操做都不用作了。偏向鎖默認是開啓的,也能夠關閉。
偏向鎖"偏",就是"偏愛"的"偏",它的意思是這個鎖會偏向於第一個得到它的程序,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。
獲取鎖的過程:
Mark Word
是否爲可偏向鎖的狀態,便是否偏向鎖即爲1即表示支持可偏向鎖,不然爲0表示不支持可偏向鎖。Mark Word
儲存的線程ID
是否爲當前線程ID
,若是是則執行同步塊,不然執行步驟3
。Mark Word
的ID
不是本線程的ID
,則經過CAS
操做去修改線程ID
修改爲本線程的ID
,若是修改爲功則執行同步代碼塊,不然執行步驟4
。鎖釋放的過程:
Mark Word
的鎖記錄指針改爲當前線程的鎖記錄,鎖升級爲輕量級鎖狀態(00)。鎖主要存在4
種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭的狀況逐漸升級,這幾個鎖只有重量級鎖是須要使用操做系統底層mutex
互斥原語來實現,其餘的鎖都是使用對象頭來實現的。須要注意鎖能夠升級,可是不能夠降級。
深刻理解Java虛擬機 Java的對象頭和對象組成詳解
JVM(三)JVM中對象的內存佈局詳解
JVM——深刻分析對象的內存佈局
啃碎併發(七):深刻分析Synchronized原理
Java Synchronized實現原理
原文地址:ddnd.cn/2019/03/22/…