synchronized 鎖的原理

synchronized 的基本認識

  在多線程併發編程中 synchronized 一直是元老級角色,不少人都會稱呼它爲重量級鎖。可是,隨着 Java SE 1.6 對synchronized 進行了各類優化以後,有些狀況下它就並不那麼重,Java SE 1.6 中爲了減小得到鎖和釋放鎖帶來的性
能消耗而引入的偏向鎖和輕量級鎖。這塊在後續咱們會慢慢展開

synchronized 的基本語法

  synchronized 有三種方式來加鎖,分別是
  1. 修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖
  2. 靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖
  3. 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。不一樣的修飾類型,表明鎖的控制粒度

synchronized 的應用

  修改前面的案例,使用 synchronized 關鍵字後,能夠達到數據安全的效果
 
    
public class Demo {
private static int count = 0;

public static void inc() {
synchronized (Demo.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}

public static void main(String[] args)
throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> Demo.inc()).start();
}
Thread.sleep(3000);
System.out.println("運行結果" + count);
}
}
 

思考鎖是如何存儲的

  能夠思考一下,要實現多線程的互斥特性,那這把鎖須要哪些因素?
  1. 鎖須要有一個東西來表示,好比得到鎖是什麼狀態、無鎖狀態是什麼狀態
  2. 這個狀態須要對多個線程共享那麼咱們來分析,synchronized 鎖是如何存儲的呢?觀察synchronized 的整個語法發現,synchronized(lock)是基於lock 這個對象的生命週期來控制鎖粒度的,那是否是鎖的
存儲和這個 lock 對象有關係呢?因而咱們以對象在 jvm 內存中是如何存儲做爲切入點,去看看對象裏面有什麼特性可以實現鎖

對象在內存中的佈局

  在 Hotspot 虛擬機中,對象在內存中的存儲佈局,能夠分爲三個區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)

 

 

synchronized 鎖的升級

  在分析 markword 時,提到了偏向鎖、輕量級鎖、重量級鎖。在分析這幾種鎖的區別時,咱們先來思考一個問題使用鎖可以實現數據的安全性,可是會帶來性能的降低。不使用鎖可以基於線程並行提高程序性能,
可是卻不能保證線程安全性。這二者之間彷佛是沒有辦法達到既能知足性能也能知足安全性的要求。hotspot 虛擬機的做者通過調查發現,大部分狀況下,加鎖的代碼不只僅不存在多線程競爭,並且老是由同一個線程
屢次得到。因此基於這樣一個機率,是的 synchronized 在JDK1.6 以後作了一些優化,爲了減小得到鎖和釋放鎖帶來的性能開銷,引入了偏向鎖、輕量級鎖的概念。所以你們會發如今 synchronized 中,鎖存在四種狀態
分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖; 鎖的狀態根據競爭激烈的程度從低到高不斷升級。

偏向鎖的基本原理

  前面說過,大部分狀況下,鎖不只僅不存在多線程競爭,而是老是由同一個線程屢次得到,爲了讓線程獲取鎖的代價更低就引入了偏向鎖的概念。怎麼理解偏向鎖呢?當一個線程訪問加了同步鎖的代碼塊時,會在對象頭中存
儲當前線程的 ID,後續這個線程進入和退出這段加了同步鎖的代碼塊時,不須要再次加鎖和釋放鎖。而是直接比較對象頭裏面是否存儲了指向當前線程的偏向鎖。若是相等表示偏向鎖是偏向於當前線程的,就不須要再嘗試得到鎖了

偏向鎖的獲取和撤銷邏輯

  1. 首先獲取鎖 對象的 Markword,判斷是否處於可偏向狀態。(biased_lock=一、且 ThreadId 爲空)
  2. 若是是可偏向狀態,則經過 CAS 操做,把當前線程的 ID寫入到 MarkWord
    a) 若是 cas 成功,那麼 markword 就會變成這樣。表示已經得到了鎖對象的偏向鎖,接着執行同步代碼塊
    b) 若是 cas 失敗,說明有其餘線程已經得到了偏向鎖,這種狀況說明當前鎖存在競爭,須要撤銷已得到偏向
     鎖的線程,而且把它持有的鎖升級爲輕量級鎖(這個操做須要等到全局安全點,也就是沒有線程在執行字
     節碼)才能執行
  3. 若是是已偏向狀態,須要檢查 markword 中存儲的ThreadID 是否等於當前線程的 ThreadID
    a) 若是相等,不須要再次得到鎖,可直接執行同步代碼塊
    b) 若是不相等,說明當前鎖偏向於其餘線程,須要撤銷偏向鎖並升級到輕量級鎖

偏向鎖的撤銷

  偏向鎖的撤銷並非把對象恢復到無鎖可偏向狀態(由於偏向鎖並不存在鎖釋放的概念),而是在獲取偏向鎖的過程當中,發現 cas 失敗也就是存在線程競爭時,直接把被偏向的鎖對象升級到被加了輕量級鎖的狀態。
對原持有偏向鎖的線程進行撤銷時,原得到偏向鎖的線程
  有兩種狀況:
  1. 原得到偏向鎖的線程若是已經退出了臨界區,也就是同步代碼塊執行完了,那麼這個時候會把對象頭設置成無鎖狀態而且爭搶鎖的線程能夠基於 CAS 從新偏向但前線程
  2. 若是原得到偏向鎖的線程的同步代碼塊還沒執行完,處於臨界區以內,這個時候會把原得到偏向鎖的線程升級爲輕量級鎖後繼續執行同步代碼塊
  在咱們的應用開發中,絕大部分狀況下必定會存在 2 個以上的線程競爭,那麼若是開啓偏向鎖,反而會提高獲取鎖的資源消耗。因此能夠經過 jvm 參數UseBiasedLocking 來設置開啓或關閉偏向鎖

 

輕量級鎖的基本原理

輕量級鎖的加鎖和解鎖邏輯

  鎖升級爲輕量級鎖以後,對象的 Markword 也會進行相應的的變化。升級爲輕量級鎖的過程:
  1. 線程在本身的棧楨中建立鎖記錄 LockRecord。
  2. 將鎖對象的對象頭中的MarkWord複製到線程的剛剛建立的鎖記錄中。
  3. 將鎖記錄中的 Owner 指針指向鎖對象。
  4. 將鎖對象的對象頭的 MarkWord替換爲指向鎖記錄的指針。

自旋鎖

  輕量級鎖在加鎖過程當中,用到了自旋鎖所謂自旋,就是指當有另一個線程來競爭鎖時,這個線程會在原地循環等待,而不是把該線程給阻塞,直到那個
得到鎖的線程釋放鎖以後,這個線程就能夠立刻得到鎖的。注意,鎖在原地循環的時候,是會消耗 cpu 的,就至關於在執行一個啥也沒有的 for 循環。
因此,輕量級鎖適用於那些同步代碼塊執行的很快的場景,這樣,線程原地等待很短的時間就可以得到鎖了。自旋鎖的使用,其實也是有必定的機率背景,在大部分同
步代碼塊執行的時間都是很短的。因此經過看似無異議的循環反而能提高鎖的性能。可是自旋必需要有必定的條件控制,不然若是一個線程執行同步代碼塊的時間很長,那麼這個線程不斷的循環反而
會消耗 CPU 資源。默認狀況下自旋的次數是 10 次,能夠經過 preBlockSpin 來修改在 JDK1.6 以後,引入了自適應自旋鎖,自適應意味着自旋的次數不是固定不變的,而是根據前一次在同一個鎖上自
旋的時間以及鎖的擁有者的狀態來決定。若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也是頗有可能再次成功,進而它將容許自旋等待持續相
對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源

輕量級鎖的解鎖

  輕量級鎖的鎖釋放邏輯其實就是得到鎖的逆向邏輯,經過CAS 操做把線程棧幀中的 LockRecord 替換回到鎖對象的MarkWord 中,若是成功表示沒有競爭。若是失敗,表示
當前鎖存在競爭,那麼輕量級鎖就會膨脹成爲重量級鎖

 

重量級鎖的基本原理

  當輕量級鎖膨脹到重量級鎖以後,意味着線程只能被掛起阻塞來等待被喚醒了。

重量級鎖的 monitor

  加了同步代碼塊之後,在字節碼中會看到一個monitorenter 和 monitorexit。每個 JAVA 對象都會與一個監視器 monitor 關聯,咱們能夠把它理解成爲一把鎖,當一個線程想要執行一段被
synchronized 修飾的同步方法或者代碼塊時,該線程得先獲取到 synchronized 修飾的對象對應的 monitor。monitorenter 表示去得到一個對象監視器。monitorexit 表示釋放 monitor 監視器的全部權,使得其餘被阻塞的線程
能夠嘗試去得到這個監視器monitor 依賴操做系統的 MutexLock(互斥鎖)來實現的, 線程被阻塞後便進入內核(Linux)調度狀態,這個會致使系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能

重量級鎖的加鎖的基本流程

  任意線程對 Object(Object 由 synchronized 保護)的訪問,首先要得到 Object 的監視器。若是獲取失敗,線程進入同步隊列,線程狀態變爲 BLOCKED。當訪問 Object 的
前驅(得到了鎖的線程)釋放了鎖,則該釋放操做喚醒阻塞在同步隊列中的線程,使其從新嘗試對監視器的獲取。

回顧線程的競爭機制

  再來回顧一下線程的競爭機制對於鎖升級這塊的一些基本流程。方便你們更好的理解加入有這樣一個同步代碼塊,存在 Thread#一、Thread#2 等多個線程
  synchronized (lock) {
    // do something
  }
  狀況一:只有 Thread#1 會進入臨界區;
  狀況二:Thread#1 和 Thread#2 交替進入臨界區,競爭不激烈;
  狀況三:Thread#1/Thread#2/Thread3… 同時進入臨界區,競爭激烈

偏向鎖

  此時當 Thread#1 進入臨界區時,JVM 會將 lockObject 的對象頭 Mark Word 的鎖標誌位設爲「01」,同時會用 CAS 操做把 Thread#1 的線程 ID 記錄到 Mark Word 中,此時進
入偏向模式。所謂「偏向」,指的是這個鎖會偏向於 Thread#1,若接下來沒有其餘線程進入臨界區,則 Thread#1 再出入臨界區無需再執行任何同步操做。也就是說,若只有
Thread#1 會進入臨界區,實際上只有 Thread#1 初次進入臨界區時須要執行 CAS 操做,之後再出入臨界區都不會有同步操做帶來的開銷。

輕量級鎖

  偏向鎖的場景太過於理想化,更多的時候是 Thread#2 也會嘗試進入臨界區, 若是 Thread#2 也進入臨界區可是Thread#1 尚未執行完同步代碼塊時,會暫停 Thread#1而且升
級到輕量級鎖。Thread#2 經過自旋再次嘗試以輕量級鎖的方式來獲取鎖

重量級鎖

  若是 Thread#1 和 Thread#2 正常交替執行,那麼輕量級鎖基本可以知足鎖的需求。可是若是 Thread#1 和 Thread#2同時進入臨界區,那麼輕量級鎖就會膨脹爲重量級鎖,意
味着 Thread#1 線程得到了重量級鎖的狀況下,Thread#2就會被阻塞

Synchronized 結合 Java Object 對象中的wait,notify,notifyAll

  前面咱們在講 synchronized 的時候,發現被阻塞的線程何時被喚醒,取決於得到鎖的線程何時執行完同步代碼塊而且釋放鎖。那怎麼作到顯示控制呢?咱們就須要
借 助 一 個 信 號 機 制 : 在 Object 對 象 中 , 提 供 了wait/notify/notifyall,能夠用於控制線程的狀態

wait/notify/notifyall 基本概念

  wait:表示持有對象鎖的線程 A 準備釋放對象鎖權限,釋放 cpu 資源並進入等待狀態。
  notify:表示持有對象鎖的線程 A 準備釋放對象鎖權限,通知 jvm 喚 醒 某 個 競 爭 該 對 象 鎖 的 線 程 X 。 線 程 Asynchronized 代碼執行結束而且釋放了鎖以後,線程 X 直
     接得到對象鎖權限,其餘競爭線程繼續等待(即便線程 X 同步完畢,釋放對象鎖,其餘競爭線程仍然等待,直至有新的 notify ,notifyAll 被調用)。
  notifyAll:notifyall 和 notify 的區別在於,notifyAll 會喚醒全部競爭同一個對象鎖的全部線程,當已經得到鎖的線程A 釋放鎖以後,全部被喚醒的線程都有可能得到對象鎖權限
  須要注意的是:三個方法都必須在 synchronized 同步關鍵字 所 限 定 的 做 用 域 中 調 用 , 否 則 會 報 錯java.lang.IllegalMonitorStateException ,意思是由於沒有同步,因此
線程對對象鎖的狀態是不肯定的,不能調用這些方法。另外,經過同步機制來確保線程從 wait 方法返回時可以感知到感知到 notify 線程對變量作出的修改
相關文章
相關標籤/搜索