Java synchronized 底層實現,原理,面試題總結

底層實現

synchronized底層實現

詳細參考:楊曉峯極客時間上的課程《Java核心技術面試精講》:第16講 | synchronized底層如何實現?什麼是鎖的升級、降級

  • synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元。

  • 發展歷程:

    • JDK6之前,Monitor 的實現完全是依靠操作系統內部的互斥鎖,因爲需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。(@所以說效率低下嘛)
    • 現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不同的 Monitor 實現,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖大大改進了其性能。
  • 偏斜鎖:JVM 會利用 CAS 操作(compare and swap),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,所以並不涉及真正的互斥鎖

  • 輕量級鎖:

    • 如果有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就需要撤銷(revoke)偏斜鎖,並切換到輕量級鎖實現。輕量級鎖依賴 CAS 操作 Mark Word 來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖;否則,進一步升級爲重量級鎖。
    • 因爲重量級鎖性能差,所以輕量級鎖又衍生出了一種鎖:自旋鎖,其實現就是自循環若干次,通過CAS操作MARK WROD試圖獲取鎖

其他鎖模型

詳細參考:王寶令極客時間上的課程《Java併發編程實戰》- 08 | 管程:併發編程的萬能鑰匙

  • 管程模型:
    • Hasen模型:要求 notify() 放在代碼的最後,這樣 T2 通知完 T1 後,T2 就結束了,然後 T1 再執行,這樣就能保證同一時刻只有一個線程執行。
    • Hoare模型:T2 通知完 T1 後,T2 阻塞,T1 馬上執行;等 T1 執行完,再喚醒 T2,也能保證同一時刻只有一個線程執行。但是相比 Hasen 模型,T2 多了一次阻塞喚醒操作。
    • MESA模型(JAVA參考實現):MESA 管程裏面,T2 通知完 T1 後,T2 還是會接着執行,T1 並不立即執行,僅僅是從條件變量的等待隊列進到入口等待隊列裏面。這樣做的好處是 notify() 不用放到代碼的最後,T2 也沒有多餘的阻塞喚醒操作。但是也有個副作用,就是當 T1 再次執行的時候,可能曾經滿足的條件,現在已經不滿足了,所以需要以循環方式檢驗條件變量。—也就是產生假喚醒

鎖變化

升級/膨脹

其實就是偏斜鎖=》輕量級鎖=》重量級鎖的過程,見第一節 #底層實現

鎖降級

鎖降級確實是會發生的,當 JVM 進入安全點(SafePoint)的時候,會檢查是否有閒置的 Monitor,然後試圖進行降級。

synchronized鎖的範圍

  • 範圍
    • 代碼塊
    • 方法
    • 對象
  • 對象鎖和類
    • 類鎖和對象鎖是分開的,(現在只是個概念,用來區分對象鎖的,是指靜態方法的鎖),程序中獲得類鎖的同時也可以獲得對象鎖。
    • 同一個類鎖和同一個類鎖是互斥的,同一個對象鎖和同一個對象鎖互斥。 非靜態方法不受類鎖的影響
    • 對象鎖與實例對象相關, 不同的對象的對象鎖不一樣,可以同時獲取兩個不同對象的對象鎖
package com.keven;

//類鎖和對象鎖的測試代碼
public class SyncTest {

    public static void main(String[] args) throws Exception {
        runObjectLockTest();
        System.out.println("finished runObjectLockTest");
        runClassLockTest();
        System.out.println("finished runClassLockTest");
        runClassObjectLockTest();
        System.out.println("finished runClassObjectLockTest");
        Thread.sleep(10000);
    }

    //測試對象鎖和類鎖是否能夠同時獲取, 可以看到兩個線程打印數據不受影響,說明不是同一個鎖
    private static void runClassObjectLockTest() {
        new Thread(SyncTest::testClassLock1, "thread1").start();
        new Thread(() -> {
            new SyncTest().testObjectLock();
        }, "thread2").start();
    }

    //測試類鎖,顯示thread1打印完成,後面thread2纔開始打印,從側面驗證獲取到的是同一個鎖
    private static void runClassLockTest() {
        new Thread(SyncTest::testClassLock1, "thread1").start();
        new Thread(SyncTest::testClassLock2, "thread2").start();

    }

    //測試對象鎖, 可以看到兩個線程打印數據不受影響, 且this對象的hash值不一樣
    private static void runObjectLockTest() {
        final SyncTest syncTest = new SyncTest();
        final Thread thread1 = new Thread(() -> {
            syncTest.testObjectLock();
        }, "thread1");

        final Thread thread2 = new Thread(() -> {
            new SyncTest().testObjectLock();
        }, "thread2");
        thread1.start();
        thread2.start();
    }

    private static synchronized void testClassLock1() {
        int i = 100;
        int count = 0;
        while ((i-- > 0) && (count++ < 10)) {
            System.out.println("method testClassLock1--" + Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(50);
            } catch (InterruptedException ie) {
            }
        }
    }

    private static synchronized void testClassLock2() {
        int i = 100;
        int count = 0;
        while ((i-- > 0) && (count++ < 10)) {
            System.out.println("method testClassLock2--" + Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(50);
            } catch (InterruptedException ie) {
            }
        }
    }

    private void testObjectLock() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " : " + this);
            int i = 100;
            int count = 0;
            while ((i-- > 0) && (count++ < 5)) {
                System.out.println("method testObjectLock--" + Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException ie) {
                }
            }
        }
    }

}

synchronized和ReentrantLock有什麼區別?

詳細可參考:楊曉峯極客時間上的課程《Java核心技術面試精講》:第15講 | synchronized和ReentrantLock有什麼區別呢?

  • synchronized 和 ReentrantLock 的性能不能一概而論:
    • 早起版本的synchronize在很多場景下性能相差較大
    • 在後續版本進行了較多的改進,在低競爭場景中表現可能優於ReentrantLock
  • 這裏所謂的公平性是指在競爭場景中,當公平性爲真時,會傾向於將鎖賦予等待時間最久的線程。公平性是減少線程“飢餓”(個別線程長期等待鎖,但始終無法獲取)情況發生的一個辦法。
  • ReentrantLock與Synchronized的區別:
    • ReentrantLock
      • 更加的靈活,但必須手動釋放鎖
        • 可通過條件控制同步
        • 可被中斷,並拋出中斷異常,釋放鎖
        • 可選擇獲取鎖的超時時間,嘗試獲取鎖
        • 可選擇是否爲公平鎖
      • 只適合代碼塊的鎖
    • synchronized
      • 無需釋放鎖,自動處理
      • 可修飾方法,類,代碼塊
      • 非公平鎖,如果阻塞則必須等待cpu調度
  • ReentrantLock與Synchronized的共通點:都是獨佔鎖或者說是排它鎖

關聯關鍵詞

  • 在上面的代碼中,我用的是 notifyAll() 來實現通知機制,爲什麼不使用 notify() 呢?
    • 這二者是有區別的,notify() 是會隨機地通知等待隊列中的一個線程,而 notifyAll() 會通知等待隊列中的所有線程。
    • 從感覺上來講,應該是 notify() 更好一些,因爲即便通知所有線程,也只有一個線程能夠進入臨界區。但那所謂的感覺往往都蘊藏着風險,實際上使用 notify() 也很有風險,它的風險在於可能導致某些線程永遠不會被通知到。@隨機的弊病不就是存在永遠不被輪到的弊病麼?這跟非公平鎖的弊病是一個意思
  • wait與sleep區別在於:
    • wait會釋放所有鎖而sleep不會釋放鎖資源.
    • wait只能在同步方法和同步塊中使用,而sleep任何地方都可以
    • wait無需捕捉異常,而sleep需要
    • sleep是Thread的方法,而wait是Object類的方法;
    • sleep方法調用的時候必須指定時間
      兩者相同點:都會讓渡CPU執行時間,等待再次調度!。補充關於二者的區別還可以看知乎的這篇帖子
  • wait()方法與sleep()方法的不同之處在於,wait()方法會釋放對象的“鎖標誌”。當調用某一對象的wait()方法後,會使當前線程暫停執行,並將當前線程放入對象等待池中,直到調用了notify()方法後,將從對象等待池中移出任意一個線程並放入鎖標誌等待池中,只有鎖標誌等待池中的線程可以獲取鎖標誌,它們隨時準備爭奪鎖的擁有權。當調用了某個對象的notifyAll()方法,會將對象等待池中的所有線程都移動到該對象的鎖標誌等待池。
  • sleep()方法需要指定等待的時間,它可以讓當前正在執行的線程在指定的時間內暫停執行,進入阻塞狀態,該方法既可以讓其他同優先級或者高優先級的線程得到執行的機會,也可以讓低優先級的線程得到執行機會。但是sleep()方法不會釋放“鎖標誌”,也就是說如果有synchronized同步塊,其他線程仍然不能訪問共享數據

常見面試題

  • synchronized和ReentrantLock的區別 @見筆記
  • 鎖什麼時候升級/降級?@見筆記
  • 類鎖和對象鎖的區別? @見筆記
  • 爲什麼JDK8中ConcurrentHashMap的鎖實現要用CAS+synchronized來取代Segment+ReentrantLock呢?
  • 爲什麼wait必須是在同步塊中的呢?@重看了一遍王寶令的課程,發現這是MESA管程模型的設計範式,硬要解釋的話可以是這樣:
    • wait是跟notify, notifyAll配對的, 是和synchronized關鍵字一起使用的
    • wait的工作原理就是wait的時候,會進入同步塊(synchronized)所對應的條件等待隊列,在其他地方使用這個關鍵字是不可進入的
    • 或者說wait所對應的管程的入口在synchronied處
  • wait與sleep區別是什麼? @見上面的筆記
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章