面試官:有了解過(guò)Synchronized嗎 說(shuō)說(shuō)看
前言
相信很多同學(xué)對(duì)synchronized的使用上不陌生,之前也給大家講解過(guò)它的使用。本篇主要帶大家深入了解一下它,大家也可以自己試著總結(jié)一下,這也是面試中常常問(wèn)到的,單純的回答它的基本使用,是驚艷不到面試官的~。
synchronized 介紹
從字面意思翻譯過(guò)來(lái)就是同步的意思,所以它也叫同步鎖,我們通常會(huì)給某個(gè)方法或者某塊代碼加上Synchronized鎖來(lái)解決多線程中并發(fā)帶來(lái)的問(wèn)題,它也是最常用,最簡(jiǎn)單的一種方法。
在Java中,鎖基本上都是基于對(duì)象而言的,所以又稱(chēng)為對(duì)象鎖, 一個(gè)類(lèi)通常只有一個(gè)class對(duì)象和n個(gè)實(shí)例對(duì)象,它們共享class對(duì)象,而我們有時(shí)候會(huì)對(duì)class對(duì)象加鎖,所以又稱(chēng)為class對(duì)象鎖。
這里大家要注意的是對(duì)象需要是一個(gè)非null的對(duì)象,我們通常也叫做對(duì)象監(jiān)視器(Object Monitor)。
重量級(jí)鎖
在JDK 1.5之前,它是一個(gè)重量級(jí)鎖,我們通常都會(huì)使用它來(lái)保證線程同步。在1.5的時(shí)候還提供了一個(gè)Lock接口來(lái)實(shí)現(xiàn)同步鎖的功能,我們只需要顯式的獲取鎖和釋放鎖。
重在哪?
在1.5的時(shí)候,Synchronized它依賴(lài)于操作系統(tǒng)底層的Mutex Lock實(shí)現(xiàn),每次釋放鎖和獲取鎖都會(huì)導(dǎo)致用戶(hù)態(tài)和內(nèi)核態(tài)的切換,從而增加系統(tǒng)性能的開(kāi)銷(xiāo),當(dāng)出現(xiàn)大并發(fā)的情況下,鎖競(jìng)爭(zhēng)會(huì)比較激烈,性能顯得非常糟糕,所以稱(chēng)為重量級(jí)鎖,所以大家往往會(huì)選擇Lock鎖。
鎖優(yōu)化
但是Synchronized又是那么的簡(jiǎn)單好用,又是官方自帶的,怎么可能放棄呢?所以在1.6之后,引入了大量的鎖優(yōu)化,比如自旋鎖,輕量級(jí)鎖, 偏向鎖等,下面我們逐個(gè)看一下。
synchronized 實(shí)現(xiàn)原理
我們了解鎖優(yōu)化之前,我們先看一下它的實(shí)現(xiàn)原理。
首先我們看下同步塊中,因?yàn)樗顷P(guān)鍵字,我們看不到源碼實(shí)現(xiàn),所以只能反編譯看一下,通過(guò) 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
我們重點(diǎn)關(guān)注monitorenter和monitorexit,那么他倆是什么意思呢?
monitorenter,如果當(dāng)前 monitor 的進(jìn)入數(shù)為 0 時(shí),線程就會(huì)進(jìn)入 monitor,并且把進(jìn)入數(shù) + 1,那么該線程就是 monitor 的擁有者 (owner)。如果該線程已經(jīng)是 monitor 的擁有者,又重新進(jìn)入,就會(huì)把進(jìn)入數(shù)再次 + 1。也就是可重入。
monitorexit,執(zhí)行 monitorexit 的線程必須是 monitor 的擁有者,指令執(zhí)行后,monitor 的進(jìn)入數(shù)減 1,如果減 1 后進(jìn)入數(shù)為 0,則該線程會(huì)退出 monitor。其他被阻塞的線程就可以嘗試去獲取 monitor 的所有權(quán)。指令出現(xiàn)了兩次,第 1 次為同步正常退出釋放鎖;第2次為發(fā)生異步退出釋放鎖。
我們?cè)賮?lái)看一下, 修飾實(shí)例方法中的表現(xiàn):
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;
}
我們重點(diǎn)關(guān)注ACC_SYNCHRONIZED,它作用就是一旦執(zhí)行到這個(gè)方法時(shí),就會(huì)先判斷是否有標(biāo)志位,如果有,就會(huì)先嘗試獲取 monitor,獲取成功才能執(zhí)行方法,方法執(zhí)行完成后再釋放 monitor。在方法執(zhí)行期間,其他線程都無(wú)法獲取同一個(gè) monitor。歸根結(jié)底還是對(duì) monitor 對(duì)象的爭(zhēng)奪,只是同步方法是一種隱式的方式來(lái)實(shí)現(xiàn)。
synchronized 在 JVM 里的實(shí)現(xiàn)就是基于進(jìn)入和退出 monitor 來(lái)實(shí)現(xiàn)的,底層則是通過(guò)成對(duì)的 MonitorEnter 和 MonitorExit 指令來(lái)實(shí)現(xiàn)。
有了以上的認(rèn)識(shí),下面我們就看看鎖優(yōu)化。
Synchronized中的鎖優(yōu)化
自適應(yīng)自旋鎖
自旋鎖,之前我們講FutureTask源碼的時(shí)候,有一個(gè)內(nèi)部方法awaitDone(),給大家有介紹過(guò),就是基于它實(shí)現(xiàn)的,今天再給大家總結(jié)一下。
它的目的是為了避免阻塞和喚醒的切換,在沒(méi)有獲得鎖的時(shí)候就不進(jìn)入阻塞,不斷地循環(huán)檢測(cè)鎖是否被釋放。但是,它也有弊端,我們通常來(lái)講,一個(gè)線程占用鎖的時(shí)間相對(duì)較短,但是萬(wàn)一占用很長(zhǎng)時(shí)間怎么辦?這樣會(huì)占用大量cpu時(shí)間,這樣會(huì)導(dǎo)致性能變差,所以在1.6引入了自適應(yīng)自旋鎖來(lái)滿(mǎn)足這樣的場(chǎng)景。
那么什么是自適應(yīng)自旋鎖呢?自旋的次數(shù)不是固定的,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定。如果此次自旋成功了,很有可能下一次也能成功,于是允許自旋的次數(shù)就會(huì)更多,反過(guò)來(lái)說(shuō),如果很少有線程能夠自旋成功,很有可能下一次也是失敗,則自旋次數(shù)就更少。這樣一來(lái),就能夠更好的利用系統(tǒng)資源。
鎖消除
鎖消除是一種鎖的優(yōu)化策略,這種優(yōu)化更加徹底,在 JVM 編譯時(shí),通過(guò)對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖。這種優(yōu)化策略可以消除沒(méi)有必要的鎖,去除獲取鎖的時(shí)間。
鎖粗化
如果一系列的連續(xù)加鎖解鎖操作,可能會(huì)導(dǎo)致不必要的性能損耗,所以引入鎖粗話(huà)的概念。意思是將多個(gè)連續(xù)加鎖、解鎖的操作連接在一起,擴(kuò)展成為一個(gè)范圍更大的鎖, 這個(gè)應(yīng)該很好理解。
偏向鎖
偏向鎖是JDK 1.6引入的,它解決的場(chǎng)景是什么呢?我們大部分使用鎖都是解決多線程場(chǎng)景下的問(wèn)題,但有時(shí)候往往一個(gè)線程也會(huì)存在這樣的問(wèn)題,偏向鎖是在單線程執(zhí)行代碼塊時(shí)使用的機(jī)制。
鎖的爭(zhēng)奪實(shí)際上是 Monitor 對(duì)象的爭(zhēng)奪,還有每個(gè)對(duì)象都有一個(gè)對(duì)象頭,對(duì)象頭是由 Mark Word 和 Klass pointer 組成的。一旦有線程持有了這個(gè)鎖對(duì)象,標(biāo)志位修改為 1,就進(jìn)入偏向模式,同時(shí)會(huì)把這個(gè)線程的 ID 記錄在對(duì)象的 Mark Word 中,當(dāng)同一個(gè)線程再次進(jìn)入時(shí),就不再進(jìn)行同步操作,大大減少了鎖獲取的時(shí)間,從而提高了性能。
輕量級(jí)鎖
我們上邊提到的偏向鎖,在多線程情況下如果偏向鎖失敗就會(huì)升級(jí)為輕量級(jí)鎖, Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級(jí)鎖的結(jié)構(gòu)。
執(zhí)行同步代碼塊之前,JVM 會(huì)在線程的棧幀中創(chuàng)建一個(gè)鎖記錄(Lock Record),并將 Mark Word 拷貝復(fù)制到鎖記錄中。然后嘗試通過(guò) CAS 操作將 Mark Word 中的鎖記錄的指針,指向創(chuàng)建的 Lock Record。如果成功表示獲取鎖狀態(tài)成功,如果失敗,則進(jìn)入自旋獲取鎖狀態(tài)。
如果自旋鎖失敗,就會(huì)升級(jí)為重量級(jí)鎖,也就是我們之前講的,會(huì)把線程阻塞,需等待喚醒。
重量級(jí)鎖
它又稱(chēng)為悲觀鎖, 升級(jí)到這種情況下,鎖競(jìng)爭(zhēng)比較激烈,占用時(shí)間也比較長(zhǎng),為了減少cpu的消耗,會(huì)將線程阻塞,進(jìn)入阻塞隊(duì)列。
synchronized就是通過(guò)鎖升級(jí)策略來(lái)適應(yīng)不同的場(chǎng)景,所以現(xiàn)在synchronized被優(yōu)化的很好,也是我們項(xiàng)目中往往都會(huì)使用它的理由。
結(jié)束語(yǔ)
本節(jié)的內(nèi)容比較多,大家好好理解,特別是鎖的升級(jí)策略。本節(jié)我們提到了Lock鎖,下一節(jié),帶大家深入學(xué)習(xí)一下Java的Lock 。