Java Synchronized 鎖的實現原理與應用 (偏向鎖,輕量鎖,重量鎖)

  1. 簡介

    在Java SE 1.6之前,Synchronized被稱爲重量級鎖.在SE 1.6之後進行了各種優化,就出現了偏向鎖,輕量鎖,目的是爲了減少獲得鎖和釋放鎖帶來的性能消耗.

  2. Synchroized的使用(三種形式)
    (1) 對於普通同步方法,鎖是當前實例對象.如下代碼示例:
    解釋:對於set和get方法來說,都是在方法上使用了同步關鍵字,所以他們是同步方法,鎖的就是當前的實例對象,怎麼理解了,看下面的main方法,就是這個new的實例對象.所以他們的鎖對象都是synchronizedMethod 這個實例.

    private int i = 0;
    
    public synchronized void setNum (int number) {
        this.i = number;
    }
    
    public synchronized int getNum () {
        return i;
    }
    
    public static void main (String[] args) {
        // 啓動兩個線程調用get和set方法
        SynchronizedMethod synchronizedMethod = new SynchronizedMethod();
        new Thread(() -> {
            synchronizedMethod.setNum(5);
        },"set").start();
        new Thread(() -> {
            int num = synchronizedMethod.getNum();
            System.out.println(num);
        },"get").start();
    }

    (2) 對於靜態同步方法,鎖是當前類的Class對象.看代碼示例:
    解釋:如下兩個方法都是靜態同步方法.所以鎖是當前類的class對象,這麼理解吧,靜態方法是類調用的,所以鎖就是這個類對象.如下代碼運行結果,只有當類的第一個靜態同步方法執行完畢,第二個才能執行.

    /**
    * synchronized 靜態方法
    */
    public class SynchroizedStaticMethod {
    
    private static int i = 0;
    
    public static synchronized void addNum () {
        for (;;) {
            i++;
            System.out.println(Thread.currentThread().getName()+"----"+i);
            if(i >= 100){
                break;
            }
        }
    }
    
    public static synchronized void getNum () {
        System.out.println(Thread.currentThread().getName()+"----"+i);
    }
    
    public static void main (String[] args) {
        new Thread(() -> {
            SynchroizedStaticMethod.addNum();
        },"addNum").start();
        new Thread(() -> {
            SynchroizedStaticMethod.getNum();
        },"getNum").start();
    
    }
    }

    一部分執行結果
    addNum----92
    addNum----93
    addNum----94
    addNum----95
    addNum----96
    addNum----97
    addNum----98
    addNum----99
    addNum----100
    getNum----100

Process finished with exit code 0
(3) 對於同步代碼塊,鎖就是Synchronized括號裏面配置的對象.如下代碼實例:
解釋:通過如下代碼可以證明鎖就是括號裏面的對象,當兩個方法是一個對象時,只能是獲取到對象鎖的方法 執行,但是是兩個鎖對象時,那麼兩個方法獲取的就是不同的鎖對象,所以結果不一樣了.

/**
 * 代碼塊
 */
public class SynchroizedCodeBlock {

    private Object object = new Object();

    public void printOne () {
        synchronized (object) {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "---" + 1);
            }
        }
    }

    public void printTwo () {
        synchronized (object) {
            System.out.println(Thread.currentThread().getName()+"---"+2);
        }
    }

    public static void main (String[] args) {
        SynchroizedCodeBlock codeBlock = new SynchroizedCodeBlock();
        new Thread(() -> {
            codeBlock.printOne();
        },"printOne").start();
        new Thread(() -> {
            codeBlock.printTwo();
        },"printTwo").start();
    }
}

執行結果
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printTwo---2

Process finished with exit code 0
改變下括號裏面的對象

public void printTwo () {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()+"---"+2);
        }
    }

執行結果(與第一次不一樣了)
printTwo---2
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1

Process finished with exit code 0
3.鎖在什麼地方(Java 對象頭)

Synchronized用的鎖是存在Java的對象頭裏的.如果對象時數組類型,則虛擬機用3個字寬存儲對象頭..Java對象頭裏的Mark Word裏默認儲存對象的HashCode.分代年齡和鎖標記位

長度 內容 說明
32/64bit Mark Word 存儲對象的hashcode或鎖信息等
32/64bit Class Metadata Address 存儲對象數據類型的指針
32/64bit Array length 數組的長度(如果當前對象時數組)

Mark Word 的狀態變化表
Java Synchronized 鎖的實現原理與應用 (偏向鎖,輕量鎖,重量鎖)

4.JSE1.6對鎖的優化(鎖的升級與對比)

在Java SE1.6中,鎖一共有4中狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提高獲得鎖和釋放鎖的效率。

(1)偏向鎖
why:在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖.
what:當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏儲存偏向的線程ID,以後該線程在進入和退出同步代碼塊時不需要進行cas操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word裏是否儲存着指向當前線程的偏向鎖。如果測試成功,表示該線程獲得了鎖。如果測試失敗,則需要在測試一下Mark Word中偏向鎖的表示是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用cas競爭鎖;如果設置了,則嘗試使用cas將對象頭的偏向鎖指向當前線程。
偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其它線程嘗試競爭偏向鎖時,持有線程纔會釋放鎖。偏向鎖的撤銷,需要等待全局安全節點(在這個時間點上沒有正在執行的字節碼)。
偏向鎖的升級:如果有線程來競爭偏向鎖,那麼就需要判斷對象頭的Mark Word的線程ID和當前線程ID是否一致,如果不一致說明發送了競爭,那麼就需要檢查擁有偏向鎖的線程是否還存活;如果沒有存活,那麼將對象頭設置爲無鎖狀態,當前線程和其它線程都可以去競爭偏向鎖;如果存活,暫停擁有偏向鎖的線程,遍歷棧幀信息,判斷這個線程是否還要使用這個鎖對象,如果還需要,就撤銷偏向鎖,升級爲輕量鎖,如果不要繼續使用,標記爲無鎖狀態,重新偏向其它線程。如果升級爲輕量鎖後,應該還是擁有鎖的線程先去執行。
(2) 輕量鎖
why:輕量鎖是爲線程競爭不是很多,每個線程的執行時間不長而準備的,因爲輕量鎖發生競爭時,不阻塞線程,而是採用的自旋;如果競爭時就阻塞線程,而鎖很快就釋放了,這個線程的狀態切換也是很大的消耗。
waht:線程在執行同步代碼塊前,jvm會先在當前線程的棧幀中創建一個用於存儲鎖記錄的空間,並將對象頭中的Mark Word替換爲爲指向鎖記錄的指針。如果成功,當前線程獲取鎖,如果失敗,表示其它線程競爭鎖,當前線程嘗試使用自旋來獲取鎖。其實就在當前線程裏面存了一份拷貝的對象頭的Mark Word(官方叫displaced Mark Word),然後把對象頭裏面Mark Word指針指向了當前線程在一開始創建的一塊鎖記錄空間,displaced Mark Word 是在釋放鎖時恢復無鎖用的。這一塊其實有些繞,就是怎麼判斷鎖這一塊具體參考這篇文檔
輕量鎖的解鎖:輕量級解鎖時,會使用cas操作將disolaced Mark Word替換回到對象頭,如果成功,則表示沒有發生競爭。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。過程如下圖所示:
Java Synchronized 鎖的實現原理與應用 (偏向鎖,輕量鎖,重量鎖)
(3) 鎖的優缺點對比

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

5.總結(有些個人理解)

偏向鎖在Java 6 和 Java 7是默認開啓的,但是它在應用程序幾秒鐘之後才激活,如果有必要可以使用jvm的參數來關閉延遲:-XX:BiasedLockingStartupDelay=0。偏向鎖是在單線程的時候使用的,我們知道單線程其實可以不用使用同步,我的理解就是爲了預防線程安全問題,因爲可能會出現多線程的情況,所以我們要預防以免帶來線程安全的問題;從上面表格可以看出加上偏向鎖性能在納秒級別,完全可以接受;偏向鎖設置是把Mark Word的是否爲偏向鎖的標識使用cas設置爲1,然後使用設置偏向鎖的裏面的線程ID,設置成功,獲取鎖,也不用釋放鎖,下次這個線程再來獲取鎖,只需要判斷偏向鎖裏面有沒有自己的線程ID即可;但是如果有線程在Mark Word的是否爲偏向鎖的標識爲1時,有線程來競爭鎖,那麼cas設置對象頭的偏向鎖指向自己線程ID時就會失敗,因爲有線程已經指向了,那麼這個時候就不滿足偏向鎖了,這時就需要執行鎖撤銷的步驟,首先等待全局安全點,然後暫停擁有偏向鎖的線程,判斷當前擁有鎖的線程是否還是存活狀態,如果沒有存活,將對象頭設置無鎖狀態;如果還存活,判斷線程是否還需要執行代碼塊,從棧幀中找到鎖記錄,如果當前線程還需要執行同步代碼塊,就證明需要鎖,這個時候升級爲輕量鎖,然後喚醒暫停線程,競爭線程開始自旋;如果不需要執行同步代碼塊了,要麼恢復到無鎖或者標記對象不適合偏向鎖(出現競爭了)。輕量鎖,說下鎖升級,輕量鎖在釋放鎖時,會使用原子的cas把原來線程裏面的displaced Mark Word替換爲對象頭,但是如果替換失敗,表示鎖競爭,鎖就升級爲重量級鎖,這個過程爲什麼會失敗了,是因爲輕量鎖的自旋是有次數的,如果達到一定的次數還沒有成功,這個自旋的線程就會升級爲重量鎖,那麼此時的對象頭的Mark Word就會被改變,然後暫停該線程,當前面的輕量鎖在釋放鎖時,會發現對象頭的Mark Word被修改,然後升級爲重量鎖,喚醒之前暫停的線程.

選自《Java 併發編程的藝術》
和參考兩位大佬的博客
鎖的升級
Synchronized 整個鎖的流程圖(厲害)
Java Synchronized 鎖的實現原理與應用 (偏向鎖,輕量鎖,重量鎖)

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