synchronized關鍵字的原理

synchronized關鍵字

什麼是synchronized

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

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.git

上述解釋的意思是:synchronized關鍵字能夠實現一個簡單的策略來防止線程干擾和內存一致性錯誤,若是一個對象對多個線程是可見的,那麼對該對象的全部讀或者寫都將經過同步的方式來進行,具體表現以下:github

  • 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(同步代碼塊和同步方法的區別)bash

深刻分析Synchronized關鍵字

線程堆棧分析

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

看下面這段程序:ide

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中能夠用來打印進程的線程堆棧信息。ui

咱們來運行這個Java程序,在終端經過top命令查看運行起來的Java程序的進程id,而後執行jstack ‘pid’。

咱們來看下打印出來的信息:

經過截圖能夠看到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相關知識的總結以及一些進階的文章記錄。

相關文章
相關標籤/搜索