鎖的筆記整理

管程

  • 含義
    管程,指的是管理共享變量以及對共享變量的操做過程html

    1. 如何解決互斥的問題
      將共享變量及其對共享變量的操做統一封裝起來,看下面那張圖
    2. 如何解決線程同步問題
      管程模型裏,共享變量和對共享變量的操做是被封裝起來的,圖中最外層的框就表明封裝的意思。框的上面只有一個入口,而且在入口旁邊還有一個入口等待隊列。當多個線程同時試圖進入管程內部時,只容許一個線程進入,其餘線程則在入口等待隊列中等待,管程裏還引入了條件變量的概念,並且 每一個條件變量都對應有一個等待隊列。經過條件變量和等待隊列,解決同步問題,想一想notify,wait的原理實現java

      • 管程的組成segmentfault

        • 一個鎖:控制管程代碼的互斥訪問
        • 入口隊列:每次只能有一個線程進入
        • 0或多個條件變量:管理共享數據的併發訪問,每一個條件變量對應有一個等待隊列
      • MESA 模型安全

        MESA 管程裏面,T2 通知完 T1 後,T2 仍是會接着執行,T1 並不當即執行(即調用了notify後,wait並後後面的代碼沒有立刻執行,只是進入了waitSet隊列等待鎖的搶奪,此時是阻塞狀態,而是等待notify釋放鎖後,wait纔開始競爭鎖,奪鎖成功後,繼續執行後續代碼),僅僅是從條件變量的等待隊列進到入口等待隊列裏面。
            這樣作的好處是 notify() 不用放到代碼的最後,T2 也沒有多餘的阻塞喚醒操做。可是也有 個反作用,就是當 T1 再次執行的時候,可能曾經知足的條件,如今已經不知足了,因此須要以循環方式檢驗條件變量併發

線程 A 和線程 B 若是想訪問共享變量 queue,只能經過調用管程提供的 enq()、deq() 方法來實現;enq()、deq() 保證互斥性,只容許一個線程進入管程

clipboard.png

synchronized wait notify notifyAll 注意點

  • 從Object對象wait方法文檔註釋有這麼一句話"This method should only be called by a thread that is the owner of this object’s monitor",看出wait方法使用,必須擁有當前對象的監視器的鎖,即wait的使用要在synchronized方法塊以內;
  • synchronized方法塊同步方式是在字節碼層面加入了monitorenter和monitorexit指令;
    synchronized方法級別同步方式則是由常量池中方法的 ACC_SYNCHRONIZED 標誌來隱式實現的,若是這個標誌被設置,若是是實例方法,則獲取對象的鎖,若是是類方法,則獲取類鎖。
  • notify 和notifyAll的選擇時要思考下使用哪個,由於notify是通知一個線程,那麼在同一個鎖下,多個線程獲取了不一樣資源的狀況,notify其中一個線程,可能不能知足循環條件的結束,這樣真正須要喚醒的線程就醒不來啦,哈哈
  • nofity使用的狀況app

    • 全部等待線程擁有相同的等待條件,若是條件不一樣,則喚醒後依舊阻塞,就浪費了機會
    • 全部等待線程被喚醒後,執行相同的操做
    • 只須要喚醒一個線程
  • 從管程角度看,synchronized 僅支持一個條件變量,調用wait方法的句柄就是條件變量
  • 從下面的代碼結果可知:notifyAll自己不會釋放鎖,須要離開synchronized 區域纔會釋放鎖;以前等待的線程被喚醒以後,會再次進行爭奪互斥鎖,獲取鎖後,狀態則恢復到進入synchronized那時狀態,繼續執行wait後面的代碼

clipboard.png

public class TestMain {
    private static List<String> list = new ArrayList<>(2);
    public static void main(String[] args) throws InterruptedException {
        TestMain testMain = new TestMain();
        list.add("1");
        Thread thread2 = new Thread(() -> testMain.apply("1","2"));
        Thread thread3 = new Thread(() -> testMain.apply("1","2"));
        Thread thread4 = new Thread(() -> testMain.free("1","2"));
        thread2.start();
        thread3.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("開始執行通知代碼塊");
        thread4.start();
    }

    public synchronized void apply(String one, String two) {
        System.out.println("獲取鎖 進入 apply = " + Thread.currentThread().getName());
        while (list.contains(one) || list.contains(two)) {
            try {
                System.out.println("開始等待,而且釋放鎖 = " + Thread.currentThread().getName());
                wait();
                System.out.println("拿到鎖了,進入條件判斷 = " + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("離開 釋放鎖 = " + Thread.currentThread().getName());
    }

    public synchronized  void free(String one ,String two)  {
        list.remove(one);
        list.remove(two);
        notifyAll();
        System.out.println("通知完成 離開代碼塊 釋放鎖 = " +   Thread.currentThread().getName());
    }
}
-- 結果
獲取鎖 進入 apply = Thread-1![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20190716202958253.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pmYzA4MjY=,size_16,color_FFFFFF,t_70)
開始等待,而且釋放鎖 = Thread-1
獲取鎖 進入 apply = Thread-0
開始等待,而且釋放鎖 = Thread-0
開始執行通知代碼塊
通知完成 離開代碼塊 釋放鎖 = Thread-2
拿到鎖了,進入條件判斷 = Thread-0
離開 釋放鎖 = Thread-0
拿到鎖了,進入條件判斷 = Thread-1
離開 釋放鎖 = Thread-1

死鎖

  • 如下四點共同發生,就會產生死鎖,因此破壞某一個條件,終止死鎖oop

    1. 互斥,共享資源 X 和 Y 只能被一個線程佔用,資源具備互斥性
    2. 佔有且等待,線程 T1 已經取得共享資源 X,在等待共享資源Y 的時候,不釋放共享資源 X
      破壞這種處境:一次性獲取須要的全部資源
      圖片來自極客時間課程
    3. 不可搶佔,其餘線程不能強行搶佔線程 T1 佔有的資源
      破壞處境:看lock實現,後續在補充
    4. 循環等待,A等B的條件,B等A的條件,相互等待
      破壞處境:按照順序申請資源,這樣不會產生互相等待資源的局面,好比從按照id大小進行鎖定對象

synchronized實現原理以及鎖的升級過程

  • 源碼實現
          對象的內存佈局,可分三個區域:header, Instance data,Padding, Hotspot採用instanceOopDesc和arrayOopDesc來描述對象頭。
          從底層源碼oop.hpp可看出,實例對象中有一個成員變量markOop類型的_mark, 從 markOop.hpp文件看出,裏面包含了鎖標識位,標識了無鎖,偏向鎖,輕量級鎖,重量級鎖的狀態值,還有分代年齡,GC標誌等信息,因此sychronized(lock)中的lock能夠用Java中任何一個對象來表示, 而鎖標識的存儲實際上就是在lock這個對象中的對象頭內。
          首先,Java中的每一個對象都派生自Object類,而每一個Java Object在JVM內部都有一個native的C++對象 oop/oopDesc 進行對應。
          其次,線程在獲取鎖的時候,實際上就是得到一個監視器對象(monitor) ,monitor能夠認爲是一個同步對象,全部的Java對象是天生攜帶monitor, 同時monitor中有一個Owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用。
  • 偏向鎖
  1. 根據鎖對象的對象頭,判斷mark是不是偏向鎖狀態,不是則建立一個markword,進行首次的CAS操做,設置偏向鎖狀態

    若是對象頭已經包含了偏向鎖狀態標識,檢測Mark Word裏是否存儲着指向當前線程的偏向鎖若是存在,
    則返回,不然 說明存在鎖競爭,須要進行偏向鎖的釋放源碼分析

  2. 偏向鎖的釋放,須要等待全局安全點(在這個時間點上沒有正在執行的字節碼),首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否還活着,
    若是線程不處於活動狀態,則將對象頭設置成無鎖狀態。
    若是線程仍然活着,則會升級爲輕量級鎖,佈局

    偏向鎖撤銷之後對象會可能會處於兩種狀態:性能

    • 一種是不可偏向的無鎖狀態,簡單來講就是已經得到偏向鎖的線程已經退出了同步代碼塊,那麼這個時候會撤銷偏向鎖, 並升級爲輕量級鎖
    • 一種是不可偏向的已鎖狀態,簡單來講就是已經得到偏向鎖的線程正在執行同步代碼塊,那麼這個時候會升級到輕量級鎖而且被原持有鎖的線程得到鎖

偏向鎖的獲取以及撤銷

  • 輕量鎖的獲取
          JVM會先在當前線程的棧幀中建立用於存儲鎖記錄的空間(LockRecord),將對象頭中的Mark Word複製到鎖記錄中,稱爲Displaced Mark Word.拷貝成功後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock Record裏的owner指針指向對象的Mark Word。
          若是輕量級鎖的更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行,不然說明多個線程競爭鎖。
          若當前只有一個等待線程,則該線程經過自旋進行等待。可是當自旋超過必定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級爲重量級鎖。
  • 輕量鎖的釋放
    將當前線程棧幀中鎖記錄空間中的Mark Word替換到鎖對象的對象頭中,若是成功表示鎖釋放成功。不然,鎖膨脹成重量級鎖,實現重量級鎖的釋放鎖邏輯

輕量級鎖的獲取

  • 爲何重量級鎖的開銷比較大呢?
    緣由是當系統檢查到是重量級鎖以後,會把等待想要獲取鎖的線程阻塞,被阻塞的線程不會消耗CPU,可是阻塞或者喚醒一個線程,都須要經過操做系統來實現,也就是至關於從用戶態轉化到內核態,而轉化狀態是須要消耗時間的
  • 重量級鎖獲取
    經過CAS將monitor的 _owner字段設置爲當前線程,若是設置成功,則直接返回
    若是以前的 _owner指向的是當前的線程,說明是重入,執行 _recursions++增長重入次數
    若是獲取鎖失敗,則須要經過自旋的方式等待鎖釋放
    若是在指定的閾值範圍內沒有得到鎖,則經過park將當前線程掛起,等待被喚醒

重量鎖的獲取

  • 總結
    偏向鎖經過對比Mark Word解決加鎖問題,避免執行CAS操做。
    而輕量級鎖是經過用CAS操做和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。
    重量級鎖是將除了擁有鎖的線程之外的線程都阻塞

以上內容來自如下資料的彙總整理:
極客時間java併發專欄
不可不說的Java「鎖」事
synchronized的源碼分析
Synchronized原理分析
深刻分析synchronized的實現原理
synchronized 的鎖膨脹過程

相關文章
相關標籤/搜索