天天看點

深入了解多線程(一)—Synchronized的實作原理深入了解多線程(一)—Synchronized的實作原理1. 實作原理2. 同步方法3. 同步代碼塊

深入了解多線程(一)—Synchronized的實作原理

synchronized,是Java中用于解決并發情況下資料同步通路的一個很重要的關鍵字。當我們想要保證一個共享資源在同一時間隻會被一個線程通路到時,我們可以在代碼中使用synchronized關鍵字對類或者對象加鎖。我想在我們剛開始學習Java的時候,一遇到多線程情況就是synchronized,相對于當時的我們來說synchronized是這麼的神奇而又強大,那個時候我們賦予它一個名字“同步”,也成為了我們解決多線程情況的百試不爽的良藥。但是,随着我們學習的進行我們知道synchronized是一個重量級鎖,相對于Lock,它會顯得那麼笨重,以至于我們認為它不是那麼的高效而慢慢摒棄它。

誠然,随着Javs SE 1.6對synchronized進行的各種優化後,synchronized并不會顯得那麼重了。接下來我們就詳細探索synchronized的實作機制、Java是如何對它進行了優化、鎖優化機制、鎖的存儲結構和更新過程。

1. 實作原理

我們知道在java中synchronized主要有兩種使用形式:同步方法和同步代碼塊。

synchronized可以保證方法或者代碼塊在運作時,同一時刻隻有一個方法可以進入到臨界區,同時它還可以保證共享變量的記憶體可見性。Java中每一個對象都可以作為鎖,這是synchronized實作同步的基礎:

(1)普通同步方法,鎖是目前執行個體對象

(2)靜态同步方法,鎖是目前類的class對象

(3)同步方法塊,鎖是括号裡面的對象接下來

我們就以一個簡單的例子來開始我們的分析,代碼如下:

public class SynchronizedTest {
    public synchronized void method1()
    {
        System.out.println("do method1");
    }
    public void method2()
    {
        synchronized (this)
        {
            System.out.println("do method2");
        }
    }
}
           

将此檔案編譯成.class檔案後,我們再使用javap工具檢視下class檔案的内容來分析下synchronized,反編譯後的截圖如下;

深入了解多線程(一)—Synchronized的實作原理深入了解多線程(一)—Synchronized的實作原理1. 實作原理2. 同步方法3. 同步代碼塊

我們可以看到Java編譯器為我們生成的位元組碼。在對于method1和method2的處理上稍有不同。也就是說。JVM對于同步方法和同步代碼塊的處理方式不同。

對于同步方法,JVM采用ACC_SYNCHRONIZED标記符來實作同步。 對于同步代碼塊。JVM采用monitorenter、monitorexit兩個指令來實作同步。

關于這部分内容,在JVM規範中也可以找到相關的描述。

2. 同步方法

The Java® Virtual Machine Specification中有關于方法級同步的介紹:

Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.
           

主要說的是: 方法級的同步是隐式的。同步方法的常量池中會有一個ACC_SYNCHRONIZED标志。當某個線程要通路某個方法的時候,會檢查是否有ACC_SYNCHRONIZED,如果有設定,則需要先獲得螢幕鎖,然後開始執行方法,方法執行之後再釋放螢幕鎖。這時如果其他線程來請求執行方法,會因為無法獲得螢幕鎖而被阻斷住。值得注意的是,如果在方法執行過程中,發生了異常,并且方法内部并沒有處理該異常,那麼在異常被抛到方法外面之前螢幕鎖會被自動釋放。

3. 同步代碼塊

同步代碼塊使用monitorenter和monitorexit兩個指令實作。 The Java® Virtual Machine Specification 中有關于這兩個指令的介紹:

monitorenter
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
monitorexit
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
           

大緻内容如下: 可以把執行monitorenter指令了解為加鎖,執行monitorexit了解為釋放鎖。 每個對象維護着一個記錄着被鎖次數的計數器。未被鎖定的對象的該計數器為0,當一個線程獲得鎖(執行monitorenter)後,該計數器自增變為 1 ,當同一個線程再次獲得該對象的鎖的時候,計數器再次自增。當同一個線程釋放鎖(執行monitorexit指令)的時候,計數器再自減。當計數器為0的時候。鎖将被釋放,其他線程便可以獲得鎖。

總結

同步方法通過ACC_SYNCHRONIZED關鍵字隐式的對方法進行加鎖。當線程要執行的方法被标注上ACC_SYNCHRONIZED時,需要先獲得鎖才能執行該方法。

同步代碼塊通過monitorenter和monitorexit執行來進行加鎖。當線程執行到monitorenter的時候要先獲得所鎖,才能執行後面的方法。當線程執行到monitorexit的時候則要釋放鎖。

每個對象自身維護這一個被加鎖次數的計數器,當計數器數字為0時表示可以被任意線程獲得鎖。當計數器不為0時,隻有獲得鎖的線程才能再次獲得鎖。即可重入鎖。