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