1. 程式人生 > >Java程式設計師必精通之—synchronized

Java程式設計師必精通之—synchronized

更多Java併發文章:https://www.cnblogs.com/hello-shf/category/1619780.html

一、簡介

相信每一個java程式設計師對synchronized都不會太陌生,尤其是在大家關心的面試環節,不瞭解synchronize?不好意思,拜拜了您嘞。synchronized作為java一個重要的同步機制,在遠古時代是被人嗤之以鼻的存在,因為在早期,synchronized屬於重量級鎖,即底層採用的是作業系統提供的Mutex lock實現的,為什麼說他是重量級的鎖呢,主要是執行緒間的切換需要作業系統從使用者態切換到核心態,開銷極其大。所以synchronized被人嗤之以鼻也就理所當然了,當然在java1.5之後呢,synchronized引入了偏向鎖,輕量級鎖,以減少對重量級鎖的依賴(減少對重量級鎖的使用是synchronized優化的終極目標),在此之後synchronized重新煥發心機,迎來了第一個春天。

二、預備知識

1,CAS

在學習synchronized之前,我們需要明白CAS(Compare and Swap)是什麼鬼,CAS呢在不同的角度有很多我們常聽到的名詞:樂觀鎖,自旋鎖。其實這個CAS在當前的各種中介軟體或者語言或者資料庫中具有相當重要的地位。synchronized鎖獲取和撤銷中正式使用的CAS自旋操作。

2,重入鎖

什麼叫重入鎖呢?很簡單,從互斥鎖的設計上來說,當一個執行緒試圖操作一個由其他執行緒持有的物件鎖的臨界資源時,將會處於阻塞狀態,但當一個執行緒再次請求自己的物件鎖鎖定的臨界資源時,是可重入的即不需要再去獲取鎖。

三、物件頭 - Mark Word

Mark World

Java中每一個物件都可作為鎖。原因是每個物件的物件頭都存在一個32bit的空間記錄著物件的基礎資訊。預設記錄物件的hashCode,分帶年齡(GC的知識),所型別,鎖標誌位(誰在拿著這把鎖)。。。
記錄這些資訊的區域叫做:Mark Word

執行緒ID即當前持有鎖的執行緒資訊。
鎖標誌位:01(預設),00(輕量級鎖),10(重量級鎖)

Monitor

monitor:監視器
你想想JVM怎麼知道哪個物件的Mark Word狀態?答案就是這個monitor,monitor是synchronized實現的另一個基礎,任何一個Java物件都有一個monitor與之關聯,當一個monitor被一個執行緒持有後,他將處於被鎖定狀態。值得注意的一點monitor只作用於重量級鎖中。

四、synchronize鎖升級過程

synchronized鎖有四個狀態:無鎖,偏向鎖,輕量級鎖,重量級鎖
synchronized鎖升級的方向:無鎖 >> 偏向鎖 >> 輕量級鎖 >> 重量級鎖
效能開銷從左到右依次增加。
鎖只會升級不會降級。

1,偏向鎖

大多數情況下,鎖不存在多執行緒競爭,總是由同一個執行緒多次獲得。
偏向鎖的使用旨在於減少對輕量級鎖的依賴,偏向鎖的加鎖和解鎖需要使用CAS自旋。
偏向鎖加鎖過程:如果一個執行緒進入同步程式碼塊(synchronized)獲得了鎖,那麼鎖就進入了偏向模式,此時Mark Word的結構也就變為偏向結構,當該執行緒再次進入同步塊(請求鎖時)將不再需要話費CAS操作來加鎖或者獲取鎖,即獲取鎖的過程只需要檢查Mark Word的鎖標記為是否為偏向鎖以及當前執行緒ID是否等於Mark Word中的threadID即可,這樣就省去了大量有關鎖申請的操作。如果當前執行緒從Mark Word獲取的鎖標誌位為01(偏向鎖)並且ThreadId=當前執行緒ID,則加鎖成果。
偏向鎖撤銷過程:
偏向鎖是用來一種競爭才釋放鎖的機制,所以當其他執行緒嘗試競爭(CAS自旋)偏向鎖時,持有偏向鎖的執行緒才有可能會釋放鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼再執行),他會首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著,如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態,該鎖會重新偏向競爭者,即Mark Word中ThreadID重新指向競爭者。如果當前執行緒依然存活,即競爭者會獲取失敗,則偏向鎖會膨脹為輕量級鎖。
關閉偏向鎖:偏向鎖在 Java 6 和 Java 7 裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才啟用,如有必要可以使用 JVM 引數來關閉延遲 -XX:BiasedLockingStartupDelay = 0。如果你確定自己應用程式裡所有的鎖通常情況下處於競爭狀態,可以通過 JVM 引數關閉偏向鎖 -XX:-UseBiasedLocking=false,那麼預設會進入輕量級鎖狀態。
當前這種偏向模式不適合鎖競爭比較激烈的多執行緒場合。

2,輕量級鎖

輕量級鎖加鎖過程:前面說到偏向鎖由輕量級鎖升級而來,偏向鎖執行在一個執行緒進入同步塊的情況下,當第二個執行緒加入鎖掙用的時候,嘗試通過CAS自旋修改Mark Word中的ThreadID,如果替換失敗,如果在一定次數內(自適應自旋機制)還是失敗,偏向鎖就會升級為輕量級鎖,當前如前面所說,偏向鎖要經歷偏向鎖撤銷 -- 到達安全點 -- 膨脹為輕量級鎖。安全點是重點。
輕量級鎖膨脹過程:當持有輕量級鎖的執行緒正在執行同步程式碼塊(持有鎖),此時又有執行緒來競爭鎖,首先該執行緒依然會通過CAS自旋替換Mark Word中的ThreadID為本執行緒的ID,在一定次數內修改失敗(當前鎖被其他執行緒持有),輕量級鎖會膨脹為重量級鎖。成功則繼續執行知道當前執行緒執行完成,釋放輕量級鎖。
輕量級鎖比較適合執行緒交替執行的場景。

3,重量級鎖

輕量級鎖因為競爭激烈,會膨脹為重量級鎖,一旦鎖膨脹為重量級鎖,執行緒切換將不是通過CAS自旋競爭來切換執行緒,而是未持有鎖的競爭者將進入阻塞態。執行緒的狀態切換都是作業系統底層的mutex lock來實現,而這個操作將意味著實現執行緒之間的切換需要從使用者態轉為核心態,這個成本是非常高的。
詳細的鎖升級過程如下圖所示:

模擬一下以上過程,假設有兩個執行緒,執行緒A和執行緒B
1,當執行緒A首先進入同步程式碼塊
1)檢查鎖狀態:判斷鎖標誌位是否為01,如果是即偏向鎖狀態
2)檢查偏向狀態:Mark Word中的ThreadID是否為當前執行緒
是:當前執行緒即執行緒A進入偏向鎖,執行同步程式碼塊。
否:進入偏向鎖競爭
2,模擬偏向鎖競爭
假設執行緒A當前持有偏向鎖,此時,執行緒B進入同步程式碼塊
1)執行緒B同樣經過1中的1)-- 2)但是Mark Word中的ThreadID == 執行緒A的ThreadID,即執行緒B獲取失敗
3)CAS自旋:執行緒B進入CAS自旋,嘗試去替換ThreadID(CAS自旋採用的是自適應自旋)
成功:獲取到偏向鎖,執行同步程式碼塊。
失敗:在一定次數內還是失敗,偏向鎖膨脹為輕量級鎖
3,偏向鎖升級為輕量級鎖
接著以上過程
1)執行緒B自旋替換ThreadID失敗,當前持有偏向鎖的執行緒A開始執行偏向鎖撤銷(等待競爭才釋放的機制)
2)執行緒A到達安全點 ,虛擬機器暫停原持有偏向鎖的執行緒即執行緒A
3)虛擬機器檢查Mark Word中ThreadID指向的執行緒(執行緒A)狀態
不活動狀態:已退出同步程式碼塊,表示執行緒A已退出競爭,執行緒B獲取到偏向鎖
活動狀態:未退出同步程式碼塊,鎖膨脹為輕量級鎖。
4,輕量級鎖競爭及膨脹過程
接著以上過程執行緒A膨脹為輕量級鎖
1)拷貝Mark Word到執行緒A的執行緒棧中,修改鎖標誌位為00,修改ThreadID指向當前執行緒即執行緒A。執行緒A被喚醒,從安全點繼續執行。
2)執行緒B開始進入同步程式碼塊,執行緒B發現鎖標誌位為00,拷貝物件頭中的Mark Word到自己的執行緒棧。
3)執行緒B自旋修改Mark Word中的ThreadID
成功:執行同步程式碼塊
失敗:輕量級鎖膨脹為重量級鎖,標誌位被修改為 10,指標指向monitor。
5,重量級鎖競爭
synchronized膨脹為重量級鎖之後,執行緒排程將依賴於作業系統底層的monitor
競爭不到鎖的執行緒將進入阻塞狀態,執行緒切換將會導致作業系統核心由使用者態到核心態的轉變(關於這個知識可以參考作業系統程序和執行緒排程的知識)。

五、synchronized優化

關於synchronized的使用,度娘一下一大把,在此就不在贅述。

1,鎖粒度優化 —— 應用層優化

synchronized作用域:
修飾靜態方法:鎖是當前物件的 Class 物件,即類鎖。
修飾非靜態方法:鎖是當前例項物件,即物件鎖。
修飾程式碼塊:鎖是 Synchonized 括號裡配置的物件(不要用Test.class這樣等同於類鎖)。
從上至下,鎖粒度是遞減的,其實最推薦使用的還是修飾同步程式碼塊,這樣儘量減少執行緒持有鎖的時間。如果你用的是類鎖,一旦鎖膨脹為重量級鎖,而類本身生命週期可以簡單地理解為=程序,鎖又不會被及時的GC掉,1.6之後對synchronize所做的偏向鎖,輕量級鎖優化等於沒做。
鎖粗化:
原則上我們需要將鎖的粒度儘量的減小,以減少鎖持有的時間。任何事情過度的追求等於浪費,如果對一個物件反覆的加鎖解鎖,也是很浪費時間的,所以當出現這種場景,儘量的需要合併同步程式碼塊,減少頻繁加鎖和解鎖的資源浪費。

2,自適應自旋鎖 —— 實現層優化

常規的自旋我們一般會這麼寫
while(true){...}
無限制的自旋是對CPU資源的極度浪費,JVM為了節省資源的浪費即更加的智慧化,採用了自旋自適應鎖,即自旋的次數不再是無限制或者固定次數,將由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來確定。

3,鎖消除 —— JVM編譯層優化

鎖消除即刪除不必要的加鎖操作。JIT編譯期,根據程式碼逃逸技術,如果判斷到一段程式碼中,堆上的資料不會逃逸出當前執行緒,那麼可以認為這段程式碼是執行緒安全的,不必加鎖。
比如如下程式碼:

1 public void add(String str1,String str2){
2     StringBuffer sb = new StringBuffer();
3     sb.append(str1).append(str2);
4 }

JVM會傻到用stringBuffer嗎?不會的,在編譯器就給你把stringBuffer方法上的synchronized給優化掉了。

   如有錯誤的地方還請留言指正。
  原創不易,轉載請註明原文地址:https://www.cnblogs.com/hello-shf/p/12091591.html

  參考文獻:

  https://www.infoq.cn/article/java-se-16-synchronized/
  https://www.cnblogs.com/paddix/p/5405678.html
  https://blog.csdn.net/baidu_38083619/article/details/8252