一步一步學多線程-synchronized
當線程執行請求synchronized方法或塊時,monitor會設置幾個虛擬邏輯數據結構來管理這些多線程。
請求的線程會首先被加入到線程排隊隊列中,線程阻塞,當某個擁有線程鎖的線程unlock之後,則排隊隊列裏的線程競爭上崗(synchronized是不公平競爭鎖),如果運行的線程調用對象wait()後就釋放鎖並進入wait線程集合那邊,當調用對象的notify()或notifyall()後,wait線程就到排隊那邊。
重量級鎖
在JVM規範中描述:每個對象有一個監視器鎖(monitor)。當monitor被占用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
1、 如果monitor的進入數為0,則該線程進入monitor,如果將進入數設置為1,該線程即為monitor的所有者。
2、 如果線程已經占有monitor,只是重新進入,則進入monitor的進入數加1.
3、 如果其他線程已經占用了monitor,則該線程進入阻塞狀態,知道monitor的進入數為0,再重新嘗試獲取monitor的所有權。
Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴於monitor對象,這就是為什麽只有在同步塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。
Synchronized是通過對象內部的一個叫做監視器鎖(monitor)來實現的。
但是其本質又是依賴於底層的操作系統的互斥鎖(Mutex Lock)來實現的。而操作系統實現線程之間的切換這就需要用戶轉換到和心態,這個成本非常高,狀態之間的轉換需要相對較長的時間,這就是為什麽Synchronized效率低的原因。
因此,這種依賴於操作系統互斥鎖(Mutex Lock)所實現的鎖我們稱之為“重量級鎖”。
當多線程環境進入synchronized區域的線程沒有競爭時,JVM並不會馬上創建重量級鎖,而是使用偏向鎖或者輕量級鎖,當存在資源競爭的情況下才會使用重量級鎖。
輕量級鎖
輕量級鎖的核心思想:被加鎖的代碼不會發生並發,如果發生並發,那就膨脹成重量級鎖(膨脹即是鎖升級)。
根據輕量級鎖的實現,我們知道雖然輕量級鎖不支持並發,遇到並發就要膨脹為重量級鎖,但是輕量級鎖可以支持多個線程以串行的方式訪問同一個鎖對象。
偏向鎖
偏向鎖的核心思想:假設加鎖的代碼自始至終只有一個線程調用,如果發現多於一個線程調用,即使沒有線程間競爭,也會把鎖升級為輕量級鎖。
自旋鎖
當線程阻塞後,如果進入排隊隊列需要CPU從用戶態轉為核心態,尤其當遇到頻繁的阻塞和喚醒對CPU來說負荷很重。統計發現,很多對象鎖的鎖定狀態持續的時間很短,此時在這麽短的時間內進行線程頻繁切換資源耗費嚴重。所以此時引出了自旋鎖的概念。
所謂“自旋”,就是monitor並不把線程阻塞放入排隊隊列,而是去執行一個無意義的循環,循環結束後看看是否鎖已釋放並直接進行競爭上崗步驟,如果競爭不到繼續自旋循環,循環過程中線程的狀態一直處於running狀態。明顯自旋鎖似的synchronized的對象鎖方式在線程之間引入了不公平。但是這樣可以保證大吞吐率和執行效率。
不過雖然自旋鎖方式省去了阻塞線程的時間和空間(隊列的維護等)開銷,但是長時間自旋也是很低效的。所以自旋的次數一般控制在一個範圍內,如10,50等(在JDK1.6中默認為10次),在超出這個範圍後,線程就進入排隊隊列。
自適應自旋鎖
就是自旋的次數是通過JVM在運行時收集的統計信息,動態調整自旋鎖的自旋次數上界。
對象頭
介紹了這幾種鎖,那麽程序是通過什麽來實現對象鎖的呢?首先來看對象頭的結構。
在Hotspot虛擬機的對象頭上主要包括兩部分數據:Mark Word(標記字段),Klass Pointer(類型指針)。其中Klass Pointer是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,Mark Word用於存儲對象自身的運行時數據,它是各種鎖的關鍵。
Mark Word中的結構大致如此
輕量級鎖的獲取和釋放
獲取鎖
1、 判斷當前對象是否處於無鎖狀態,若是,則JVM首先將當前線程的棧幀中建立一個名為所記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝。否則執行步驟3。
2、 JVM利用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,如果成功表示競爭到鎖,則將鎖標誌位變成00(表示此對象處於輕量級鎖狀態),執行同步操作,如果失敗則執行步驟3。
3、 判斷當前對象的Mark Word是否指向當前線程的棧幀,如果是則表示當前線程已經持有當前對象的鎖,則直接執行同步代碼塊;否則只能說明該鎖對象已經被其他線程搶占了,這時輕量級鎖需要膨脹為重量級鎖,鎖標誌位變成10,後面等待的線程將會進入阻塞狀態。
釋放鎖
輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下
1、 取出在獲取輕量級鎖保存在Mark Word中的數據;
2、 用CAS操作將取出的數據替換當前對象的Mark Word中,如果成功,則說明鎖釋放成功,否則執行3.
3、 如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要在釋放所的同時需要喚醒被掛起的線程。
偏向鎖的釋放和獲取
獲取鎖
1、 檢測Mark Word是否為可偏向狀態,即是否為偏向鎖1,鎖表示為01;
2、 若為可偏向狀態,則測試線程ID是否為為當前線程ID,如果是,則執行步驟5,否則執行步驟3。
3、 如果線程ID不是當前線程ID,則通過CAS操作競爭鎖,競爭成功,則將Mark Word的線程ID替換為當前線程ID,否則執行步驟4。
4、 通過CAS競爭鎖失敗,證明當前存在多線程競爭情況,當到達全局安全點,獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼塊。
5、 執行同步代碼塊。
釋放鎖
偏向鎖的釋放采用了一種只有競爭才會釋放鎖的機制,線程會是不會主動釋放偏向鎖,需要等待其他線程來競爭。偏向鎖的撤銷需要等待全局安全點,步驟如下:
1、 暫停擁有偏向鎖的線程,判斷鎖對象是否還處於被鎖定狀態
2、 撤銷偏向鎖,恢復到無鎖狀態或輕量級鎖的狀態。
參考資料
synchronized、鎖、多線程同步的原理是咋樣的
深入分析synchronized的實現原理
一步一步學多線程-synchronized