面試官:有了解過Synchronized嗎 說說看
前言
相信很多同學對synchronized的使用上不陌生,之前也給大家講解過它的使用。本篇主要帶大家深入了解一下它,大家也可以自己試著總結一下,這也是面試中常常問到的,單純的回答它的基本使用,是驚艷不到面試官的~。
synchronized 介紹
從字面意思翻譯過來就是同步的意思,所以它也叫同步鎖,我們通常會給某個方法或者某塊代碼加上Synchronized鎖來解決多線程中并發帶來的問題,它也是最常用,最簡單的一種方法。
在Java中,鎖基本上都是基于對象而言的,所以又稱為對象鎖, 一個類通常只有一個class對象和n個實例對象,它們共享class對象,而我們有時候會對class對象加鎖,所以又稱為class對象鎖。
這里大家要注意的是對象需要是一個非null的對象,我們通常也叫做對象監視器(Object Monitor)。
重量級鎖
在JDK 1.5之前,它是一個重量級鎖,我們通常都會使用它來保證線程同步。在1.5的時候還提供了一個Lock接口來實現同步鎖的功能,我們只需要顯式的獲取鎖和釋放鎖。
重在哪?
在1.5的時候,Synchronized它依賴于操作系統底層的Mutex Lock實現,每次釋放鎖和獲取鎖都會導致用戶態和內核態的切換,從而增加系統性能的開銷,當出現大并發的情況下,鎖競爭會比較激烈,性能顯得非常糟糕,所以稱為重量級鎖,所以大家往往會選擇Lock鎖。
鎖優化
但是Synchronized又是那么的簡單好用,又是官方自帶的,怎么可能放棄呢?所以在1.6之后,引入了大量的鎖優化,比如自旋鎖,輕量級鎖, 偏向鎖等,下面我們逐個看一下。
synchronized 實現原理
我們了解鎖優化之前,我們先看一下它的實現原理。
首先我們看下同步塊中,因為它是關鍵字,我們看不到源碼實現,所以只能反編譯看一下,通過 javap -v **.class。
public static void main(String[] args) {
synchronized(Demo.class) {
System.out.println("hello");
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/thread/base/Demo
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String hello
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
我們重點關注monitorenter和monitorexit,那么他倆是什么意思呢?
monitorenter,如果當前 monitor 的進入數為 0 時,線程就會進入 monitor,并且把進入數 + 1,那么該線程就是 monitor 的擁有者 (owner)。如果該線程已經是 monitor 的擁有者,又重新進入,就會把進入數再次 + 1。也就是可重入。
monitorexit,執行 monitorexit 的線程必須是 monitor 的擁有者,指令執行后,monitor 的進入數減 1,如果減 1 后進入數為 0,則該線程會退出 monitor。其他被阻塞的線程就可以嘗試去獲取 monitor 的所有權。指令出現了兩次,第 1 次為同步正常退出釋放鎖;第2次為發生異步退出釋放鎖。
我們再來看一下, 修飾實例方法中的表現:
class Demo {
public synchronized void hello() {
System.out.println("hello");
}
}
public synchronized void hello();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 25: 0
line 26: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/thread/base/Demo;
}
我們重點關注ACC_SYNCHRONIZED,它作用就是一旦執行到這個方法時,就會先判斷是否有標志位,如果有,就會先嘗試獲取 monitor,獲取成功才能執行方法,方法執行完成后再釋放 monitor。在方法執行期間,其他線程都無法獲取同一個 monitor。歸根結底還是對 monitor 對象的爭奪,只是同步方法是一種隱式的方式來實現。
synchronized 在 JVM 里的實現就是基于進入和退出 monitor 來實現的,底層則是通過成對的 MonitorEnter 和 MonitorExit 指令來實現。
有了以上的認識,下面我們就看看鎖優化。
Synchronized中的鎖優化
自適應自旋鎖
自旋鎖,之前我們講FutureTask源碼的時候,有一個內部方法awaitDone(),給大家有介紹過,就是基于它實現的,今天再給大家總結一下。
它的目的是為了避免阻塞和喚醒的切換,在沒有獲得鎖的時候就不進入阻塞,不斷地循環檢測鎖是否被釋放。但是,它也有弊端,我們通常來講,一個線程占用鎖的時間相對較短,但是萬一占用很長時間怎么辦?這樣會占用大量cpu時間,這樣會導致性能變差,所以在1.6引入了自適應自旋鎖來滿足這樣的場景。
那么什么是自適應自旋鎖呢?自旋的次數不是固定的,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果此次自旋成功了,很有可能下一次也能成功,于是允許自旋的次數就會更多,反過來說,如果很少有線程能夠自旋成功,很有可能下一次也是失敗,則自旋次數就更少。這樣一來,就能夠更好的利用系統資源。
鎖消除
鎖消除是一種鎖的優化策略,這種優化更加徹底,在 JVM 編譯時,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖。這種優化策略可以消除沒有必要的鎖,去除獲取鎖的時間。
鎖粗化
如果一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗,所以引入鎖粗話的概念。意思是將多個連續加鎖、解鎖的操作連接在一起,擴展成為一個范圍更大的鎖, 這個應該很好理解。
偏向鎖
偏向鎖是JDK 1.6引入的,它解決的場景是什么呢?我們大部分使用鎖都是解決多線程場景下的問題,但有時候往往一個線程也會存在這樣的問題,偏向鎖是在單線程執行代碼塊時使用的機制。
鎖的爭奪實際上是 Monitor 對象的爭奪,還有每個對象都有一個對象頭,對象頭是由 Mark Word 和 Klass pointer 組成的。一旦有線程持有了這個鎖對象,標志位修改為 1,就進入偏向模式,同時會把這個線程的 ID 記錄在對象的 Mark Word 中,當同一個線程再次進入時,就不再進行同步操作,大大減少了鎖獲取的時間,從而提高了性能。
輕量級鎖
我們上邊提到的偏向鎖,在多線程情況下如果偏向鎖失敗就會升級為輕量級鎖, Mark Word 的結構也變為輕量級鎖的結構。
執行同步代碼塊之前,JVM 會在線程的棧幀中創建一個鎖記錄(Lock Record),并將 Mark Word 拷貝復制到鎖記錄中。然后嘗試通過 CAS 操作將 Mark Word 中的鎖記錄的指針,指向創建的 Lock Record。如果成功表示獲取鎖狀態成功,如果失敗,則進入自旋獲取鎖狀態。
如果自旋鎖失敗,就會升級為重量級鎖,也就是我們之前講的,會把線程阻塞,需等待喚醒。
重量級鎖
它又稱為悲觀鎖, 升級到這種情況下,鎖競爭比較激烈,占用時間也比較長,為了減少cpu的消耗,會將線程阻塞,進入阻塞隊列。
synchronized就是通過鎖升級策略來適應不同的場景,所以現在synchronized被優化的很好,也是我們項目中往往都會使用它的理由。
結束語
本節的內容比較多,大家好好理解,特別是鎖的升級策略。本節我們提到了Lock鎖,下一節,帶大家深入學習一下Java的Lock 。