1. 程式人生 > >Java Synchronized 鎖的實現原理與應用 (偏向鎖,輕量鎖,重量鎖)

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

edm star 使用場景 安全 amp add [] 升級 ddr

  • 簡介

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

  • 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 的狀態變化表
    技術分享圖片

    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替換為為指向鎖記錄的指針。如果成功,當前線程獲取鎖,如果失敗,表示其它線程競爭鎖,當前線程嘗試使用自旋來獲取鎖。這一塊其實有些繞,就是怎麽判斷鎖這一塊具體參考這篇文檔
    輕量鎖的解鎖:輕量級解鎖時,會使用cas操作將disolaced Mark Word替換回到對象頭,如果成功,則表示沒有發生競爭。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。過程如下圖所示:
    技術分享圖片
    (3) 鎖的優缺點對比

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

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