synchronized關鍵字的原理

JasonGaoH發表於2019-08-02

synchronized關鍵字

什麼是synchronized

JDK官網對synchronized關鍵字有個比較權威的解釋。

Synchronized keyword enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object's variables ard done through synchronized methods.

上述解釋的意思是:synchronized關鍵字可以實現一個簡單的策略來防止執行緒干擾和記憶體一致性錯誤,如果一個物件對多個執行緒是可見的,那麼對該物件的所有讀或者寫都將通過同步的方式來進行,具體表現如下:

  • synchronized關鍵字提供了一種鎖的機制,能夠確保共享變數的互斥訪問,從而防止資料不一致的問題出現。
  • synchronized關鍵字包括monitor enter和monitor exit兩個JVM指令,它能夠保證在任何時候任何執行緒執行到monitor enter成功之前都必須從主記憶體中獲取資料,而不是快取中,在monitor exit執行成功之後,共享變數被更新後的值必須刷入主記憶體。
  • synchronized的執行嚴格遵守java happens-before 規則,一個monitor exit指令之前必定要有一個monitor enter。

synchronized關鍵字的用法

synchronized可以用於對程式碼塊或方法進行修飾,而不能夠用於對class以及變數進行修飾。

  • 同步方法
public synchronized void sync() {
    //...
}
複製程式碼
  • 同步方法塊
private final Object lock = new Object();
public void sync() {
    synchronized(lock) {
        //...
    }
}
複製程式碼

關於同步程式碼塊和同步方法的區別之前寫過一個關於這個對比,具體可以看這篇文章。 java中的synchronized(同步程式碼塊和同步方法的區別)

深入分析Synchronized關鍵字

執行緒堆疊分析

synchronized關鍵字提供了一種互斥機制,也就是說在同一時刻,只能有一個執行緒訪問同步資源。

看下面這段程式:

import java.util.concurrent.TimeUnit;

public class TestSync {
	
	private final static Object lock = new Object();
	
	public void accessResource() {
		synchronized(lock) {
			try {
				TimeUnit.MINUTES.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) {
		final TestSync sync = new TestSync();
		for(int i =0;i<5;i++) {
			new Thread(){
				@Override
				public void run() {
					sync.accessResource();
				}	
			}.start();
		}
	}
}

複製程式碼

上面的程式碼定義一個方法accessResource,並且使用synchronized來對程式碼進行同步,同時定義了5個執行緒呼叫accessResource方法,由於synchronized的互斥性,只能有一個執行緒獲得lock的monitor鎖,其他執行緒只能進入阻塞狀態,等待獲取lock的monitor鎖。

針對這個monitor鎖我們如何從執行緒堆疊資訊來看呢?

其實,jstack命令在Java中可以用來列印程式的執行緒堆疊資訊。

我們來執行這個Java程式,在終端通過top命令檢視執行起來的Java程式的程式id,然後執行jstack ‘pid’。

我們來看下列印出來的資訊:

synchronized關鍵字的原理

通過截圖可以看到Thread-0持有monitor<0x00000007955f2130>的鎖並且處於休眠狀態中,而其他幾個執行緒則是處於BLOCKED狀態中,它們是在等待著獲取monitor<0x00000007955f2130>的鎖。

JVM指令分析

從JVM指令角度再來分析synchronized關鍵字。

我們可以使用javap這個命令來對上面這個TestSync類生成的class位元組碼進行反編譯,得到下面的JVM指令。

Compiled from "TestSync.java"
public class main.TestSync {
  static {};
    Code:
       0: new           #3                  // class java/lang/Object
       3: dup
       4: invokespecial #10                 // Method java/lang/Object."<init>":()V
       7: putstatic     #13                 // Field lock:Ljava/lang/Object;
      10: return

  public main.TestSync();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: return

  public void accessResource();
    Code:
       0: getstatic     #13                 // Field lock:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter
       6: getstatic     #20                 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
       9: ldc2_w        #26                 // long 10l
      12: invokevirtual #28                 // Method java/util/concurrent/TimeUnit.sleep:(J)V
      15: goto          23
      18: astore_2
      19: aload_2
      20: invokevirtual #32                 // Method java/lang/InterruptedException.printStackTrace:()V
      23: aload_1
      24: monitorexit
      25: goto          31
      28: aload_1
      29: monitorexit
      30: athrow
      31: return
    Exception table:
       from    to  target type
           6    15    18   Class java/lang/InterruptedException
           6    25    28   any
          28    30    28   any

  public static void main(java.lang.String[]);
    Code:
       0: new           #1                  // class main/TestSync
       3: dup
       4: invokespecial #44                 // Method "<init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: goto          27
      13: new           #45                 // class main/TestSync$1
      16: dup
      17: aload_1
      18: invokespecial #47                 // Method main/TestSync$1."<init>":(Lmain/TestSync;)V
      21: invokevirtual #50                 // Method main/TestSync$1.start:()V
      24: iinc          2, 1
      27: iload_2
      28: iconst_5
      29: if_icmplt     13
      32: return
}
複製程式碼

從上面的指令中可以看到,在accessResource()方法中,先後出現了一個monitor enter和兩個monitor exit。

我們主要選取accessResource()這部分程式碼塊來重點分析。

public void accessResource();
    Code:
       0: getstatic     #13                 //①獲取lock
       3: dup
       4: astore_1
       5: monitorenter                      //②執行monitorenter JVM指令
       6: getstatic     #20                 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
       9: ldc2_w        #26                 // long 10l
      12: invokevirtual #28                 // Method java/util/concurrent/TimeUnit.sleep:(J)V
      15: goto          23                  //③跳轉到23行
      18: astore_2
      19: aload_2
      20: invokevirtual #32                 // Method java/lang/InterruptedException.printStackTrace:()V
      23: aload_1                           //④
      24: monitorexit                       //⑤ 執行monitor exit JVM指令
      25: goto          31
      28: aload_1
      29: monitorexit
      30: athrow
      31: return
複製程式碼

首先①獲取到lock引用,然後執行②monitorenter JVM指令,休眠結束後goto至③monitorexit的位置 (astore_n表示儲存引用到本地變數表;aload_n表示從本地變數表載入應用;getstatic表示從class中獲取靜態屬性)

monitorenter

每一個物件都與一個monitor相關聯,一個monitor的lock的鎖只能被一個執行緒在同一時間獲得,在一個執行緒嘗試獲得與物件關聯的monitor的所有權時會發生如下的幾件事情。

  • 如果monitor的計數器為0,則意味著該monitor的lock還沒有被獲得,,某個執行緒獲得之後將立即對該計數器加一,從此該執行緒就是這個monitor的所有者了。
  • 如果一個已經擁有該執行緒所有權的執行緒重入,則會導致monitor的計數器再次累加。
  • 如果monitor已經被其他執行緒所擁有,則其他執行緒嘗試獲取該monitor所有權時,會被陷入阻塞狀態直到monitor變為0,才能再次嘗試獲取對monitor的所有權。

monitorexit

釋放對monitor的所有權,想要釋放某個物件關聯的monitor所有權的前提是,你曾經擁有了所有權。釋放monitor所有權的過程比較簡單,就是將monitor的計數器減一,如果計數器的結果為0,則意味著該執行緒不在擁有對該monitor的所有權,通俗地講就是解鎖。

synchronized的鎖優化

在虛擬機器規範對monitorenter和monitorexit的行為描述中,有兩點是需要特別注意的,首先,synchronized同步塊對於同一條執行緒是可重入的,不會出現自己鎖死自己的問題。其次,同步課在已進入的執行緒執行完以前,會阻塞後面其他執行緒的進入。

Java的執行緒是對映到作業系統執行緒上的,吐過要阻塞或喚醒一個執行緒,都需要作業系統來幫忙完成,這就需要從使用者態切到核心態,因此狀態轉換需要耗費很多的處理器時間,對於簡單的同步塊(如被synchronized修飾的getter或setter方法),狀態轉換消耗的時間有可能比使用者程式碼執行的時間還要長。所以synchronized是Java語言中的一個重量級的操作。

其實大多數時候,共享資料的鎖定狀態一般只會持續很短的一段時間,為了這段時間去掛起和恢復執行緒其實並不值得。

如果物理機上有多個處理器,可以讓多個執行緒同時執行的話。我們就可以讓後面來的執行緒“稍微等一下”,但是並不放棄處理器的執行時間,看看持有鎖的執行緒會不會很快釋放鎖。這個“稍微等一下”的過程就是自旋。

自旋鎖在JDK 1.4中已經引入,在JDK 1.6中預設開啟。只是將當前執行緒不停地執行迴圈體,不進行執行緒狀態的改變,所以響應速度更快,因為上面剛說到,執行緒的狀態切換會耗費很多CPU時間。但當執行緒數不停增加時,效能下降明顯,因為每個執行緒都需要執行,佔用CPU時間。如果執行緒競爭不激烈,並且保持鎖的時間段,適合使用自旋鎖。

最後,歡迎大家關注我的KnowledgeSummary,主要是關於Java以及Android相關知識的總結以及一些進階的文章記錄。

相關文章