syncronized原理解析

​多線程進行併發訪問資源時需要進行鎖同步,否則會出現兩個線程之間的計算交疊造成邏輯錯誤。在java中常用的關鍵字syncronized就是用來進行加鎖同步的。下面我們就來聊一聊syncronized的實現原理。 

有過C++開發經驗的同學都知道,在C++中進行鎖同步通常會使用mutex(互斥量)。互斥量是操作系統提供給我們的一種能力,通過他可以實現資源的搶佔與訪問隔離。調用僞代碼如下:

std::mutex mutex;mutex.lock();access_resource();mutex.unlock;

上面是一個常見的加鎖邏輯(這是一個悲觀鎖的邏輯)。在java當中,我們同樣依賴mutex實現訪問隔離。通過將資源(每個java對象)與一個對應的mutex關聯起來,然後在對象訪問前後分別調用上面的邏輯。但是在調用mutex會使得線程發生狀態轉換,導致線程在內核態和用戶態之間進行切換從而導致性能問題,因此我們稱mutex爲一個重型鎖。加鎖時線程狀態轉換關係如下:

mutex調用之後線程會切換到BLOCKED狀態(讓渡出線程時間分片),當競爭鎖成功線程又會被喚醒至RUNNABLE狀態。這兩者之間的轉換對操作系統的開銷比較大(線程會在內核態和用戶態之間轉移)。因此JVM在實現syncronized關鍵字時進行了針對性的性能優化。

優化思想是儘量採用樂觀鎖替代悲觀鎖,從而避免mutex的調用。這是基於一個假設,大部分情況下,線程的競爭是很弱的,且線程佔用資源的時間分片會很短,也就是說鎖會很快被釋放。說到樂觀鎖,它的實現原理是CAS(Compare And Swap)思想。CAS是操作系統提供的一種原子能力。使用CAS實現樂觀鎖的僞代碼如下:

 //flag作爲鎖標誌位關聯訪問資源boolean flag = false;if (CAS(flag, expect=false, true)) {         //加鎖成功訪問資源         accessResource();         //解鎖         flag= false;} else {         //不成功就重試         retry();}

這樣就避免了mutex的調用。但是樂觀鎖也有自身的缺點,就是當競爭激烈的時候,線程會不停的重試,不會讓渡出線程資源,造成計算資源的浪費。所以在重度競爭場景還是使用mutex比較合理。因此我們會傾向於依次進行不加鎖,樂觀鎖,mutex的逐級嘗試。這就是syncronized的鎖升級實現。 

第一級偏向鎖

偏向鎖傾向於認爲當前資源訪問不存在競爭。在本線程佔有資源之後不會有其他線程併發訪問(如果有就需要進行鎖升級)。偏向鎖也是基於CAS樂觀鎖的實現。上面提到CAS實現樂觀鎖需要將資源(java對象)與一個flag鎖標誌位關聯起來。syncronized利用了每個java對象頭中的Markword空間。Markword是每個java對象頭中一個4字節空間(32機器)。當偏向鎖模式時,其各bit功能如下:

搶佔線程ID(23bit)

Epoch(2bit)

分代年齡(4bit)

是否偏向鎖(1bit)

鎖標誌位(2bit)

偏向鎖運行過程如下:

  • 初始化時搶佔線程ID爲空

  • 線程A比較Markword佔用線程ID字段,如果指向自己則直接訪問資源。否則通過CAS(expect=null, set=self)操作將Markword中的線程ID指向自己,搶佔成功,否則加鎖失敗升級鎖。佔鎖成功後,在自己的線程棧幀中創建LockRecord指向Markword。LockRecord還記錄本線程加鎖次數(爲可重入服務,後面會介紹)

  • 線程訪問對象資源

  • 線程A解鎖後,清空線程棧幀中的LockRecord,但是不清理Markword中的佔用線程ID

從上面的過程可以看出當線程A佔鎖成功後,後面再次訪問都是無同步訪問(CAS都不需要,因爲Markword中線程ID指向了自己)。但是當線程解鎖後,如果有線程B訪問,此時也會造成鎖升級。因此偏向鎖是一個基於無競爭假設的實現。

那麼是不是線程B的CAS加鎖一定會失敗那,也不是,syncronized中設置了鎖批量重偏向和批量撤銷的機制(結合epoch實現,不再展開),當滿足一定條件會重置搶佔線程ID,所以線程B的併發訪問有一定的概率可以通過CAS加鎖成功,避免鎖升級。 

第二級輕量鎖

前面提到當出現兩個線程競爭時需要將偏向鎖升級爲輕量鎖。輕量鎖是一種基於CAS重試機制實現的輕量級樂觀鎖。實現原理與之前提到的CAS樂觀鎖一致。同步資源的搶佔標誌也是利用對象頭中的Markword字段,輕量鎖Markword字段如下:

指向線程棧Markword副本指針(30bit)

鎖標誌位(2bit)

升級爲輕量鎖時,搶佔線程會將對象頭中的Markword拷貝一份副本存放在自己的線程棧幀中。鎖搶佔時通過CAS操作將對象頭中的Markword副本指針指向自己,如果設置成功則搶佔成功。搶佔示意如下:

解鎖時通過CAS將對象頭中的副本指針置空。如果線程競爭失敗,會進行空轉重試(自旋)。如果線程多次嘗試失敗,那說明當前競爭異常激烈,需要將鎖升級爲重型鎖。 

第三級重型鎖

重型鎖通過mutex互斥量實現。mutex被包裝在Monitor對象中。資源對象通過對象頭中Markword對象存放的Monitor指針與鎖對象相關聯。此時Markword結構如下:

指向Monitor對象指針(30bit)

鎖標誌位(2bit)

下面重點看下Monitor類。Monitor是一個由C++實現的類,主要類成員如下:

  • _owner指向持有Monitor對象的線程

  • _EntryList等待競爭處於BLOCKED狀態的線程存放隊列

  • _WaitSet調用了wait方法,而進入等待狀態的線程存放隊列

  • _recursions當前持鎖線程重入次數

當線程通過mutex佔鎖成功後就將_owner指向佔鎖線程。搶佔失敗的線程會被操作系統掛起進入到_EntryList中等待資源釋放後被喚醒。整個過程線程轉移關係如下:

WaitSet是持有鎖的線程,在代碼中主動調用了wait()接口後被操作系統掛起的線程。當有其他線程調用了notify接口後,掛起線程會被喚醒,重新競爭鎖資源繼續運行。線程wait,notify通過條件變量實現(condition variable)。

_recursions成員是用來實現鎖重入機制的。鎖重入是指程序中實現嵌套加鎖,例如:

在第二次加鎖時,當前線程只要檢查到_owner字段指向自己,就可以避免mutex調用,直接獲取鎖資源,同時將recursions字段加一。解鎖時通過減一操作,當recursions歸零時實現真正的解鎖操作(前面的偏性鎖和輕量鎖中的LockRecord也有加鎖次數字段實現響應的功能)。以上就是我對syncronized的實現過程的理解。主要思想就是樂觀地看待競爭,儘量通過輕量級的同步操作解決問題,逐步升級應對措施。

純屬個人理解,歡迎交流。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章