Java併發編程(二)之synchronized

前言

上一篇文章我們學習了併發編程中的第一個核心知識點線程,如果有小夥伴沒看過可以點這裏Java併發編程(一)之線程。這篇文章就帶小夥伴們一起學習一下第二個核心知識點—synchronized,這也是面試中只要問道併發必問的知識點。

正文

爲什麼會出現synchronized

多線程帶來的問題

線程的合理使用能夠提升程序的處理性能,主要有兩個方面,第一個是能夠利用多核 cpu 以及超線程技術來實現線程的並行執行;第二個是線程的異步化執行相比於同步執行來說,異步執行能夠很好的優化程序的處理性能提升併發吞吐量。但同時也帶來了很多問題,比如多線程對於共享數據的訪問帶來的安全性問題:對於一個共享變量count來說,如果只有一個線程A對它進行訪問修改操作,是不會有什麼問題的,但是如果多個線程同時都對這個共享變量進行操作,就會出現線程不安全的問題(操作之後得到的實際值跟預期值不一樣)

線程安全性

線程安全是併發編程中的重要關注點。對於線程安全性,本質上是管理對於數據狀態的訪問,而且這個這個狀態通常是共享的、可變的。共享,是指這個數據變量可以被多個線程訪問;可變,指這個變量的值在它的生命週期內是可以改變的。一個對象是否是線程安全的,取決於它是否會被多個線程訪問,以及程序中是如何去使用這個對象的。所以,如果多個線程訪問同一個共享對象,在不需額外的同步以及調用端代碼不用做其他協調的情況下,這個共享對象的狀態依然是正確的(正確性意味着這個對象的結果與我們預期規定的結果保持一致),那說明這個對象是線程安全的。下面我們寫一個例子看看:

public class UnsafeDemo {

    private static int count = 0;

    public static void inc(){
        try {
            //這裏讓線程睡眠一會,表示實際代碼中的業務處理
            Thread.sleep(1);
            count++;
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0;i < 1000;i++){
            //創建1000個線程對count進行“++”操作,預期結果應該是1000
            new Thread(()->UnsafeDemo.inc()).start();
        }
        Thread.sleep(3000);
        System.out.println("運行結果:" + UnsafeDemo.count);
    }
}

執行結果
在這裏插入圖片描述
可以看出跟我們預期的值是不一樣的,多運行幾次會發現結果也可能不一樣,這就是線程安全的問題。

如何解決線程安全問題

問題的本質是多個線程併發對共享數據進行了訪問、修改等操作,如果我們對多線程進行控制,使得第一個線程在對共享數據沒有操作完之前其他線程不得對共享數據進行操作,那麼線程安全問題不就解決了嗎?如何實現對線程的控制呢?接下來就引入了“鎖”的概念,通過對線程“上鎖”實現對線程的控制。什麼是“鎖”呢?“鎖”是處理併發的一種同步手段,而如果需要達到前面我們說的一個目的,那麼這個鎖一定需要實現互斥的特性。Java中提供的加鎖方式就是使用synchronized關鍵字。

synchronized的基本認識

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

synchronized的使用

1.修飾代碼塊

指定加鎖對象,對給定的對象加鎖,進入同步代碼塊之前需要先獲得指定對象的鎖。這種方式加鎖,鎖的範圍是synchronized後面一對{}所包含的代碼塊。
代碼示例 :

public class SyncDemo1 implements Runnable {

    private static int count = 0;
    private static int flag = 0;

    @Override
    public void run() {
        //這裏的代碼塊是沒有加鎖的
        for (int j = 0;j < 5;j++){
            flag++;
        }
        //這裏的代碼塊進行加鎖
        synchronized (this){
            for (int i = 0;i < 5;i++){
                count++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncDemo1 syncDemo1 = new SyncDemo1();
        for (int i = 0;i < 10000;i++){
//            SyncDemo1 syncDemo1 = new SyncDemo1();
            Thread thread = new Thread(syncDemo1,"Thread" + i);
            thread.start();
        }
        Thread.sleep(300);
        System.out.println("運行結果---count:" + count + "   flag:" + flag);
    }

}

執行結果:
在這裏插入圖片描述
分析:
這裏count的操作是加鎖的,flag的操作是沒加鎖的,count的實際結果與我們預期值是一樣的,所以它是線程安全的,而flag的值有可能會與預期值不同,所以它是線程不安全的。這裏鎖住的是SyncDemo1對象的實例,當所有線程公用一個SyncDemo1實例的時候,各個線程之間是“阻塞式”執行的,也就只有執行完該代碼塊才能釋放該對象鎖,下一個線程才能執行並鎖定該對象,因爲鎖住的都是同一個對象實例,各個線程之間是互斥的。如果把SyncDemo1的創建放在for循環裏面,讓每一個線程都去創建一個SyncDemo1對象實例,那麼各個線程就會同時(交替)執行,因爲此時有多少SyncDemo1對象實例就有多少鎖(synchronized只是給當前對象實例加鎖),各個鎖之間是互不干擾的,此時count的值也會有可能與預期值不同。

2.修飾實例方法

修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。這種方式加鎖,鎖的範圍是整個示例方法。
代碼示例:

public class SyncDemo1 implements Runnable {

    private static int count = 0;

    @Override
    public synchronized void run() {
        for (int i = 0;i < 5;i++){
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncDemo1 syncDemo1 = new SyncDemo1();
        for (int i = 0;i < 10000;i++){
            Thread thread = new Thread(syncDemo1,"Thread" + i);
            thread.start();
        }
        Thread.sleep(300);
        System.out.println("運行結果---count:" + count);
    }

}

執行結果:
在這裏插入圖片描述
分析:
修飾實例方法和修飾代碼塊很類似,唯一不同的是兩種修飾方式鎖的作用範圍有所不同。不管是修飾實例方法還是修飾代碼塊,都要注意兩點:

  1. 線程安全的前提是,多個線程操作的是同一個實例,如果多個線程作用於不同的實例,那麼線程安全是無法保證的。
  2. 同一個實例的多個實例方法上有synchronized,這些方法都是互斥的,同一時間只允許一個線程操作同一個實例的其中的一個synchronized方法。

3.修飾靜態方法

修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖,也就是說鎖的對象就是當前類的Class對象。
代碼實例:

public class SyncDemo1 implements Runnable {

    private static int count = 0;

    @Override
    public void run() {
        inc();
    }

    public synchronized static void inc(){
        for (int i = 0;i < 5;i++){
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
//        SyncDemo1 syncDemo1 = new SyncDemo1();
        for (int i = 0;i < 10000;i++){
            SyncDemo1 syncDemo1 = new SyncDemo1();
            Thread thread = new Thread(syncDemo1,"Thread" + i);
            thread.start();
        }
        Thread.sleep(300);
        System.out.println("運行結果---count:" + count);
    }

}

執行結果:
在這裏插入圖片描述
分析:
靜態方法是屬於類的,而不是屬於類的實例對象的,我們可以看到,把SyncDemo1 對象的創建放入for循環中,也就是每個線程都有一個SyncDemo1 對象的實例,但是因爲我們是爲SyncDemo1 類的靜態方法加的鎖,也就是鎖住的是這個類,所以就算是不同的實例,也依然不會影響線程安全。

鎖是如何存儲的

如果要實現多線程的互斥特效,鎖需要具備以下兩種因素:

  1. 鎖需要有一個東西來表示,比如獲得鎖是什麼狀態、無鎖狀態是什麼狀態。
  2. 這個狀態需要對多個線程共享。

通過對上面鎖的基本使用進行分析,synchronized(lock)是基於lock這個對象的生命週期來控制鎖的力度範圍的,那麼鎖的存儲和這個lock對象有什麼關係呢?接下來我們以對象在JVM中的存儲作爲切入點看看對象中是如何實現鎖的,首先我們看一下對象的結構圖:
在這裏插入圖片描述
對象頭最核心的兩個就是Mark Word和Class Metadata Address,Mark Word結構如上圖所示,類型指針Class Metadata Address主要是用於指向對象的類元數據,JVM通過這個指針確定該對象是哪個類的實例。Mark word 記錄了對象和鎖有關的信息,當某個對象被synchronized 關鍵字當成同步鎖時,那麼圍繞這個鎖的一系列操作都和 Mark word 有關係。Mark Word 在 32 位虛擬機的長度是 32bit、在 64 位虛擬機的長度是 64bit。Mark Word 裏面存儲的數據會隨着鎖標誌位的變化而變化,Mark Word 可能變化爲存儲以下 5 種結構:
在這裏插入圖片描述
每一個鎖都對應一個monitor對象,在HotSpot虛擬機中它是由ObjectMonitor實現的(C++實現)。每個對象都存在着一個monitor與之關聯,對象與其monitor之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷燬或當線程試圖獲取對象鎖時自動生成,但當一個monitor被某個線程持有後,它便處於鎖定狀態。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //鎖計數器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有兩個隊列_WaitSet和_EntryList,用來保存ObjectWaiter對象列表(每個等待鎖的線程都會被封裝ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入_EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。
monitor對象存在於每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是爲什麼Java中任意對象可以作爲鎖的原因,同時也是notify/notifyAll/wait等方法存在於頂級對象Object中的原因。
在這裏插入圖片描述

爲什麼任何對象都可以實現鎖

Java 中的每個對象都派生自 Object 類,而每個Java Object 在 JVM 內部都有一個 native 的 C++對象
oop/oopDesc 進行對應。線程在獲取鎖的時候,實際上就是獲得一個監視器對象(monitor) ,monitor 可以認爲是一個同步對象,所有的Java 對象是天生攜帶 monitor。多個線程訪問同步代碼塊時,相當於去爭搶對象監視器修改對象中的鎖標識。

synchronized鎖的升級

使用鎖能夠實現數據的安全性,但是會帶來性能的下降。不使用鎖能夠基於線程並行提升程序性能,但是卻不能保證線程安全性。這兩者之間似乎是沒有辦法達到既能滿足性能也能滿足安全性的要求。hotspot 虛擬機的作者經過調查發現,大部分情況下,加鎖的代碼不僅僅不存在多線程競爭,而且總是由同一個線程多次獲得鎖。所以synchronized在JDK1.6之後做了一定的升級優化,使得它不在那麼重了,爲了減少獲得鎖和釋放鎖帶來的性能開銷,進而引入了偏向鎖、輕量級鎖的概念。因此在 synchronized 鎖中存在四種狀態分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖; 鎖的狀態根據競爭激烈的程度從低到高不斷升級。

偏向鎖

偏向鎖的基本原理

大部分情況下,鎖不僅僅不存在多線程競爭,而是總是由同一個線程多次獲得,爲了讓線程獲取鎖的代
價更低就引入了偏向鎖的概念。當一個線程訪問加了同步鎖的代碼塊時,會在對象頭中存儲當前線程的 ID,後續這個線程進入和退出這段加了同步鎖的代碼塊時,不需要再次加鎖和釋放鎖。而是直接比較對象頭裏面是否存儲了指向當前線程的偏向鎖。如果存在表示偏向鎖是偏向於當前線程的,就不需要再嘗試獲得鎖了。

偏向鎖的獲取和撤銷邏輯

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

偏向鎖的撤銷

偏向鎖的撤銷並不是把對象恢復到無鎖可偏向狀態(因爲偏向鎖並不存在鎖釋放的概念),而是在獲取偏向鎖的過程中,發現 cas 失敗也就是存在線程競爭時,直接把被偏向的鎖對象的鎖狀態升級到輕量級鎖的狀態。
對原持有偏向鎖的線程進行撤銷時,原獲得偏向鎖的線程有兩種情況:
(a)原獲得偏向鎖的線程如果已經退出了臨界區,也就是同步代碼塊執行完了,那麼這個時候會把對象頭設置成無鎖狀態並且爭搶鎖的線程可以基於 CAS 重新偏向但前線程。
(b)如果原獲得偏向鎖的線程的同步代碼塊還沒執行完,處於臨界區之內,這個時候會把原獲得偏向鎖的線程升級爲輕量級鎖後繼續執行同步代碼塊。
下面以圖解方式解釋上述邏輯:
在這裏插入圖片描述
在這裏插入圖片描述
注:在我們的應用開發中,絕大部分情況下一定會存在 2 個以上的線程競爭,那麼如果開啓偏向鎖,反而會提升獲取鎖的資源消耗。所以可以通過 jvm 參數UseBiasedLocking 來設置開啓或關閉偏向鎖也就是直接使用輕量級鎖。

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

鎖升級爲輕量級鎖之後,對象的 Markword 也會進行相應的的變化。升級爲輕量級鎖的過程:

  1. 線程在自己的棧楨中創建鎖記錄 LockRecord。
  2. 將鎖對象的對象頭中的MarkWord複製到線程的剛剛創建的鎖記錄中。
  3. 將鎖記錄中的 Owner 指針指向鎖對象。
  4. 將鎖對象的對象頭的 MarkWord替換爲指向鎖記錄的指針。
    在這裏插入圖片描述
    在這裏插入圖片描述

自旋鎖

輕量級鎖在加鎖過程中,用到了自旋鎖所謂自旋,就是指當有另外一個線程來競爭鎖時,這個線程會在原地循環等待,而不是把該線程給阻塞,直到那個獲得鎖的線程釋放鎖之後,這個線程就可以馬上獲得鎖的。注意,鎖在原地循環的時候,是會消耗 cpu 的,就相當於在執行一個啥也沒有的 for 循環。所以,輕量級鎖適用於那些同步代碼塊執行的很快的場景,這樣,線程原地等待很短的時間就能夠獲得鎖了。自旋鎖的使用,其實也是有一定的概率背景,在大部分同步代碼塊執行的時間都是很短的。所以通過看似無異議的循環反而能提升鎖的性能。但是自旋必須要有一定的條件控制,否則如果一個線程執行同步代碼塊的時間很長,那麼這個線程不斷的循環反而會消耗 CPU 資源。默認情況下自旋的次數是 10 次,可以通過 preBlockSpin 來修改。

在 JDK1.6 之後,引入了自適應自旋鎖,自適應意味着自旋的次數不是固定不變的,而是根據前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。

輕量級鎖的解鎖

輕量級鎖的鎖釋放邏輯其實就是獲得鎖的逆向邏輯,通過CAS 操作把線程棧幀中的 LockRecord 替換回到鎖對象的MarkWord 中(簡單來說就是將鎖狀態置爲無鎖,偏向鎖位爲"0",鎖標識位爲"01"),如果成功表示沒有競爭。如果失敗,表示當前鎖存在競爭,那麼輕量級鎖就會膨脹成爲重量級鎖。
在這裏插入圖片描述

重量級鎖的基本原理

因爲自旋會消耗CPU,爲了避免過多的自旋,一旦鎖升級成重量級鎖,就不會再 恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。當輕量級鎖膨脹到重量級鎖之後,意味着線程只能被掛起阻塞來等待被喚醒了。
重量級鎖的狀態下,對象的mark word爲指向一個monitor對象的指針。我們對這段代碼進行反編譯,看看class文件中的信息:

public class MonitorDemo {

    private static int i = 0;

    public static void method(){
        synchronized (MonitorDemo.class){
            i++;
        }
        System.out.println(i);
    }

    public static void main(String[] args) {
        method();
    }
}

在這裏插入圖片描述
加了同步代碼塊以後,在字節碼中會看到monitorenter 和 monitorexit。從反編譯的同步代碼塊可以看到同步塊是由monitorenter指令進入,然後monitorexit釋放鎖,在執行monitorenter之前需要嘗試獲取鎖,如果這個對象沒有被鎖定,或者當前線程已經擁有了這個對象的鎖,那麼就把鎖的計數器加1。當執行monitorexit指令時,鎖的計數器也會減1。當獲取鎖失敗時會被阻塞,一直等待鎖被釋放。

但是爲什麼會有兩個monitorexit呢?其實第二個monitorexit是來處理異常的,仔細看反編譯的字節碼,正常情況下第一個monitorexit之後會執行goto指令,而該指令轉向的就是23行的return,也就是說正常情況下只會執行第一個monitorexit釋放鎖,然後返回。而如果在執行中發生了異常,第二個monitorexit就起作用了,它是由編譯器自動生成的,在發生異常時處理異常然後釋放掉鎖。

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

重量級鎖加鎖流程

在這裏插入圖片描述
任意線程對 Object(Object 由 synchronized 保護)的訪問,首先要獲得 Object 的監視器。如果獲取失敗,線程進入同步隊列,線程狀態變爲 BLOCKED。當訪問 Object 的前驅(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對監視器的獲取。

回顧線程競爭機制

爲了方便理解,這裏舉一個例子:

 synchronized (lock){
            //TODO
        }

加入有這樣一個同步代碼塊,現在存在Thread#1、Thread#2等多個線程去訪問同步代碼塊,會有以下幾種情況:
情況一:只有Thread#1會進入臨界區
情況二:Thread#1和Thread#2交替進入臨界區,競爭不激烈
情況三:Thread#1/Thread#2/Thread#2…同時進入臨界區,競爭很激烈

偏向鎖

此時當 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#1,最後Thread#2 通過自旋再次嘗試以輕量級鎖的方式來獲取鎖。

重量級鎖

如果 Thread#1 和 Thread#2 正常交替執行,那麼輕量級鎖基本能夠滿足鎖的需求。但是如果 Thread#1 和 Thread#2同時進入臨界區(Thread#2一直通過自旋獲取不到鎖),那麼輕量級鎖就會膨脹爲重量級鎖,意味着 Thread#1 線程獲得了重量級鎖的情況下,Thread#2就會被阻塞。

三種鎖的優缺點

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級別的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊的場景
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度 自旋時間過長,會消耗CPU 適用於同步塊執行速度非常快的場景/追求響應時間
重量級鎖 線程競爭不會自旋,不消耗CPU 線程阻塞,響應時間緩慢 適用於同步塊執行速度較慢的場景

總結

Java虛擬機中synchronized關鍵字的實現,按照代價由高到低可以分爲重量級鎖、輕量鎖和偏向鎖三種。

  • 重量級鎖會阻塞、喚醒請求加鎖的線程。它針對的是多個線程同時競爭同一把鎖的情況。JVM採用了自適應自旋,來避免線程在面對非常小的synchronized代碼塊時,仍會被阻塞、喚醒的情況。
  • 輕量級鎖採用CAS操作,將鎖對象的mark world替換爲一個指針,指向當前線程棧上的一塊空間(鎖記錄),存儲着鎖對象原本的mark world。它針對的是多個線程在不同時間段申請同一把鎖的情況。
  • 偏向鎖只會在第一次請求時採用CAS操作,在鎖對象的標記字段中記錄下當前線程的內存地址。在之後的運行過程中,持有該偏向鎖的線程的加鎖操作將直接返回。它針對的是鎖僅會被同一線程持有的情況。

下面以一張圖總結synchronized原理:
在這裏插入圖片描述

結束語

寫這篇文章,也參考了很多大佬的文章,收穫頗豐,希望對小夥伴們也有所幫助。下面推薦兩篇比較好的文章深入理解synchronized底層原理synchronized—深入總結。因爲篇幅原因,文中沒有詳解CAS機制,這也是一塊很重要的內容,有興趣的小夥伴可以去看這個CAS原理 基礎篇

練習、用心、持續------致每一位追夢人!加油!!!

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