synchronized 鎖的原理


synchronized 的基本認識

  在多線程並發編程中 synchronized 一直是元老級角色,很多人都會稱呼它為重量級鎖。但是,隨着 Java SE 1.6 對synchronized 進行了各種優化之后,有些情況下它就並不那么重,Java SE 1.6 中為了減少獲得鎖和釋放鎖帶來的性
能消耗而引入的偏向鎖和輕量級鎖。這塊在后續我們會慢慢展開

synchronized 的基本語法

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

synchronized 的應用

  下面通過一個實例展示以上三種加鎖方案,使用 synchronized 關鍵字后,可以達到數據安全的效果

//靜態方法,作用於當前類對象加鎖,這個和修飾實例方法,作用於當前實例加鎖類似,唯一區別是方法是用static修飾的,靜態屬於類級別,普通方法屬於對象實例級別
public class Demo {
private static int count = 0;
  
   //這里添加synchronized等價於下方添加synchronized,都屬於類級別
public /*synchronized*/ 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=1、且 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#1、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 線程對變量做出的修改
 
 
                    我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3ug7193lkwcg4


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM