JVM 多線程高併發--synchroized

1.synchroized 基本用法

1.1語義

  • 原子性:確保線程互斥的訪問同步代碼

  • 可見性:保證共享變量的修改能夠即時可見,其實通過對Java內存模型中“對一個變量unlock操作之前,必須同步到主內存中;如果對一個變量進行lock操作,則將會清空工作內存中此變量的值,在執行引擎使用此變量前,需要重新從主內存中load操作或者assign操作初始化變量值”來保證。

  • 有序性:有效的解決重排問題,即“一個unlock操作先行發生(happen-before)於後面對同一個鎖的lock操作”

    更多解釋參考:https://km.sankuai.com/page/316714826

1.2 常見用法

從語法上將,synchronized可以把認可一個非null對象作爲“鎖”,在HotSpot JVM中,鎖有一個專門的名字:對象監視器(object Monitor)

synchronized總共有三種用法

  • 當synchronized作用在實例方法時,監視器鎖(monitor)便是對象實例(this);

  • 當synchronized作用在靜態方法時,監視器鎖(monitor)便是對象的Class 實例,因爲Class 數據存在於永久代,因此靜態方法鎖相當於該類的一個全局鎖;

  • 當synchronized作用在某一個對象實例時,監視器鎖(monitor)便是括號括起來的對象實例;

注意:synchronized內置鎖鎖一種對象鎖(鎖的是對象而非引用變量),作用粒度是對象,可以用來實現對臨界資源的同步互斥訪問;是可重入的,其最大的作用是可以避免死鎖。如:

子類同步方法調用了父類同步方法,如果沒有可重入的特性,則會發生死鎖。

2.同步原理

數據同步需要依賴鎖,那鎖的同步又依賴誰?synchroized給出的答案誰在軟件層面依賴於JVM,而j.u.c.Lock給出的答案是在硬件層面依賴特殊的CPU指令。

當一個線程訪問同步代碼塊時,首先是需要獲得鎖才能執行同步代碼,當推出或者拋出異常時必須要要釋放鎖,那麼它是如何實現這個機制的呢?可以看一段簡單的代碼


public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

查看反編譯後結果:

反編譯結果

  1. monitorenter:每個對象都是一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:

    1. 如果monitor的進入數爲0,則該線程進入monitor,然後進入數設置爲1,該線程即爲monitor的所有者;

    2. 如果線程已經有該monitor,只是重新進入,則進入monitor的進入數+1;

    3. 如果其他線程已經用了monitor,則該線程進入阻塞狀態,知道monitor的進入數爲0,再嘗試獲取monitor的所有權。

  2. monitorexit:執行monitorexit的西安城必須是objectref 所對應的monitor的所有者。指令執行時,monitor的進入數減1,如果減1後進入數爲0 ,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor所阻塞的線程可以嘗試去獲取這個monitor的所有權。

monitorexit指令出現了兩次,第一次爲同步正常退出釋放鎖;第二次爲發生異常退出釋放鎖

通過上面兩段描述,我們應該很清楚看到synchronized的實現原理,synchronized語義底層是通過一個monitor的對象完成,其實wait/notify等方法也依賴於monitor對象,這就是爲什麼只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常原因。

 

再來看下同步方法塊


public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

查看反編譯後結果

 

從反編譯解決來看,方法的同步並沒有通過指令monitorenter 和monitorexit來完成(理論上其實也可以通過這兩條指令來實現),不過相對於普通方法,其常量池中多了 ACC_SYNCHROIZED標識符。JVM就死活根據該標識符來實現同步方法的同步。

當方法調用時,調用指令將會檢查方法的ACC_SYNCHROIEZED訪問標識是否被設置,如果設置來,執行線程將先獲取monitor,獲取成功之後才能執行方法體,方法執行完成後釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。

兩種同步方式本質上沒有區別,只是方法的同步是一種隱式的方法來實現,無需通過字節碼來完成。兩個指令的執行時JCVM通過調用操作系統的互斥源於mutex來實現,被阻塞的線程會被掛起、等待重新調度,會導致“用戶態和內核態”兩個態之間來回切換,對性能有較大影響。

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