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