併發編程(二):Synchornized關鍵字

一,Synchorized語法演示

    1,類方法演示;靜態類同步是對類對象加鎖,基於同一類對象的操作都會在阻塞隊列等待執行(通過自定義類加載器可以實現類的不同加載)

package com.gupao.concurrent;

/**
 * 類同步方法
 * @author pj_zhang
 * @create 2019-09-25 21:55
 **/
public class StaticMethodSynchorized {

    public static void main(String[] args) {
        // 線程一執行
        new Thread(() -> {
            System.out.println("線程_1嘗試競爭鎖");
            doSomething();
        }, "線程_1").start();
        // 線程二執行
        new Thread(() -> {
            System.out.println("線程_2嘗試競爭鎖");
            doSomething();
        }, "線程_2").start();
    }

    public synchronized static void doSomething() {
        System.out.println(Thread.currentThread().getName() + "獲取到當前鎖");
        // 自旋,不釋放鎖對象
        for (;;) {}
    }
}

    2,實例方法演示;實例類同步是對類實例加鎖,對同一類實例對象的加鎖操作,需要等待執行;如果此處構建兩個實例,並由兩個實例分別在線程_1和線程_2中調用同步方法,則不會存在鎖競爭;

package com.gupao.concurrent;

/**
 * 實例同步方法
 * @author pj_zhang
 * @create 2019-09-25 21:55
 **/
public class EntityMethodSynchorized {

    // 此處構建同一對象進行同步方法處理
    // 如果對象不同,則對象攜帶鎖標誌位不同,
    // 不同的實例執行實例方法,會根據當前實例鎖狀態進行加鎖處理
    // 則線程1和線程2會全部執行
    public static EntityMethodSynchorized synchorized = new EntityMethodSynchorized();

    public static void main(String[] args) {
        // 線程一執行
        new Thread(() -> {
            System.out.println("線程_1嘗試競爭鎖");
            synchorized.doSomething();
        }, "線程_1").start();
        // 線程二執行
        new Thread(() -> {
            System.out.println("線程_2嘗試競爭鎖");
            synchorized.doSomething();
        }, "線程_2").start();
    }

    public synchronized void doSomething() {
        System.out.println(Thread.currentThread().getName() + "獲取到當前鎖");
        // 自旋,不釋放鎖對象
        for (;;) {}
    }
}

    3,同步代碼塊演示(同樣分實例鎖和類鎖);同步代碼塊的鎖對象可以分爲類鎖和類對象鎖,分別可以對應第一步和第二步演示的解釋;

package com.gupao.concurrent;

/**
 * 同步代碼塊處理
 * @author pj_zhang
 * @create 2019-09-25 22:07
 **/
public class CodeBlockSynchronized {

    // 此處構建同一對象進行同步方法處理
    // 如果對象不同,則對象攜帶鎖標誌位不同,
    // 不同的實例執行實例方法,會根據當前實例鎖狀態進行加鎖處理
    // 則線程1和線程2會全部執行
    public static Object object = new Object();

    public static void main(String[] args) {
        // 線程一執行
        new Thread(() -> {
            System.out.println("線程_1嘗試競爭鎖");
            doSomething();
        }, "線程_1").start();
        // 線程二執行
        new Thread(() -> {
            System.out.println("線程_2嘗試競爭鎖");
            doSomething();
        }, "線程_2").start();
    }

    public static void doSomething() {
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "獲取到當前鎖");
            for (;;) {}
        }
    }

}

二,Synchorized鎖狀態存儲

    1,javap編譯查看鎖狀態

        1.1 同步方法:從下圖可以看到,jvm對同步方法添加了一個ACC_SYNCHRONIZED的flag。jvm底層會對該字節指令進行解析,添加monitor監視器,其他線程在執行到該方法時,會首先獲取類對象(類方法)或者實例對象(實例方法)的monitor監視器。如果獲取到,則加鎖成功,如果獲取不到,說明存在線程正在執行,進入同步隊列進行線程等待;正在執行的線程執行完成後,會釋放monitor監視器,並喚醒在同步隊列中的線程進行線程等待

        1.2 同步代碼塊:從同步代碼塊中可以很清晰看到兩個字節指令,monitorenter(進入監視器)和monitorenter(退出監視器),監視器退出後,會喚醒同步隊列中等待的線程,進行鎖搶佔;相對於同步方法,同步代碼塊在加鎖和釋放鎖中更可控

    2,對象的內存佈局

        2.1 從上圖可以看出,Synchorized的加鎖處理其實是對對象的monitor進行操作,則可以從鎖的內存佈局進行入手查看鎖實現

        2.2 對象在內存中分三個區域存儲,分別是對象頭(header),實例數據(Instance Data)已經對齊填充(Padding);其中對象頭分爲instanceOopDesc(instanceOop.hpp)和ArrayOopDesc(ArrayOop.hpp)描述,instanceOopDesc又繼承自oopDesc(oop.hpp)。對普通實例對象的定義,oopDesc包含兩個成員,分別是_mark和_metadata。_mark也就是Mark Word(markOop.hpp)記錄了對象和鎖有關的消息。Mark Word在虛擬機在32位和64位虛擬機的內存佈局如下,其中定義了分代年齡(4位),是否偏向鎖(1位)及鎖標誌位(2位)

    3,鎖在對象中的存儲:Mark Word裏面存儲的數據會隨着鎖標誌位的變化而變化,Mark Word大致表現爲下面五種變化;線程執行會從無鎖狀態獲取偏向鎖,在存在線程競爭時,從偏向鎖升級爲輕量級鎖,輕量級鎖在指定的自旋次數後,如果還沒有獲取到鎖,則膨脹爲重量級鎖;膨脹爲重量級鎖後,未搶佔到鎖的線程狀態爲BLOKED,進入同步隊列,等待持有鎖的線程執行完成後喚醒

三,Synchorized鎖升級

    1,鎖升級的意義:加鎖能夠實現數據的安全性,但是同時帶來性能的下降。對每一次加鎖進行線程阻塞和調度額外增加CPU的壓力,因此JVM提出了鎖升級的概念,用來減少頻繁獲取鎖和釋放鎖所帶來的性能開銷。鎖存在四種狀態:無鎖,偏向鎖,輕量級鎖,重量級鎖,鎖的狀態根據競爭的激烈程度從低到高不斷升級。

    2,無鎖

        * 對象初始化狀態,還沒有被加鎖

    3,偏向鎖,

        3.1 偏向鎖的獲取流程

            a,首先獲取鎖對象的Mark Word,判斷是否處於可偏向狀態(biased_lock爲1,且threadId爲空)

            b,如果是可偏向狀態,則通過CAS操作,把當前線程ID寫入到Mark Word

                 * 如果CAS成功,表示已經獲取的鎖對象的偏向鎖,可以繼續執行同步代碼

                 * 如果CAS失敗,說明已經有其他線程獲取了偏向鎖,說明存在線程競爭;這時候需要撤銷Mark Word中偏向鎖的線程ID,並把鎖狀態由偏向鎖變更爲輕量級鎖(這個操作需要等到全局安全點執行)

            c,如果是已偏向狀態,則檢查Mark Word中存儲的線程ID是否是當前線程

                 * 如果是,不需要再次獲得鎖,直接執行同步代碼

                 * 如果不是,則繼續進行鎖升級

        3.2,偏向鎖撤銷:並不是把鎖對象恢復到無鎖可偏向狀態(偏向鎖並不存在鎖釋放的概念),而是在獲取偏向鎖的過程中,發現CAS失敗也就是存在鎖競爭的過程中,直接把偏向鎖升級到輕量級鎖的狀態;

        3.3,偏向鎖獲取及撤銷流程圖

    4,輕量級鎖

        4.1,輕量級鎖的加鎖流程:鎖升級爲輕量級鎖後,鎖對象的Mark Word會進行相應的變化

              a,線程會在自己的棧幀中創建鎖記錄:LockRecord

              b,將鎖對象的Mark Word複製到線程剛剛創建的LockRecord中

              c,將LockRecord中的Owner指針指向鎖對象

              d,將鎖對象的Mark Word替換爲指向鎖記錄的指針

        4.2,輕量級鎖解鎖流程:輕量級鎖的釋放邏輯就是加鎖邏輯的逆向邏輯

               a,通過CAS操作把棧幀中的LockRecord替換回到鎖對象的Mark Word中,如果替換成功說明沒有競爭

               b,如果替換失敗,表示當前鎖存在競爭,輕量級鎖會膨脹爲重量級鎖

        4.3,輕量級鎖執行原理

               a,輕量級鎖的執行原理就是自旋,當存在線程來競爭鎖時,該線程會通過執行一段無意義的循環不斷嘗試獲取鎖,而不是直接進行線程阻塞,等到正在執行的線程釋放鎖後,該線程會直接獲取到鎖進行代碼執行

               b,因爲線程在自旋是,會存在CPU消耗,所以自選鎖適用於那些同步代碼快執行很快的場景

               c,自選鎖在JVM中默認重試次數是10,可通過 preBlockSpin 參數設置,JDK1.6後,引入了自適應自旋鎖,即JVM會根據前一次的自旋時間動態判斷當前的自旋次數,更合理的進行線程自旋規劃

        4.4,輕量級鎖的加鎖和解鎖流程圖

    5,重量級鎖:當線程膨脹爲重量級鎖後,則意味着線程只能被掛起阻塞等待被喚醒

        5.1,重量級鎖的加鎖流程

              a,重量級鎖加鎖,就是就是獲取鎖對象的 Monitor 監視器,如果獲取成功,則執行

              b,Monitor監視器獲取失敗,線程進入同步隊列,線程轉態爲BLOCKED

              c,噹噹前獲取鎖的線程釋放Monitor監視器後,會喚醒在同步隊列中等待的線程,重新進行鎖競爭

        5.2,重量級鎖的釋放流程

              a,重量級鎖的釋放,其實就是同步代碼執行完成後,對Monitor監視器的釋放

        5.3,重量級鎖加鎖和釋放鎖流程圖

四,基於Synchorized的wait()/notify()流程分析

    1,對wait()和notify()的理解,可以在前一步重量級鎖競爭同步隊列的基礎上添加一層阻塞隊列進行理解(參考AQS的Condition類)

    2,多線程在無wait()情況下進行併發訪問時,會有一道線程競爭到鎖進行執行,其他線程在同步隊列中等候,當線程釋放鎖時,會喚醒在同步隊列中的線程進行鎖爭搶,並重復該流程

    3,此時存在線程,在線程執行過程時進入線程等待(wait()),線程等待需要超時喚醒或者手動喚醒。在線程等待期間,線程不參與鎖爭搶,此時該線程存放在同步隊列是不合適的,需要存在的阻塞隊列,等線程被喚醒或者自動喚醒後,再從阻塞隊列遷移到同步隊列參與鎖爭搶

    4,流程圖如下

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