淺說Synchronized的底層實現(xiàn)原理
一、前言
synchronized關(guān)鍵字用來保證在同一時刻只有一個線程可以執(zhí)行被它修飾的變量或者代碼塊。
這一篇中,只涉及synchronized的底層實現(xiàn)原理,不涉及對synchronized效率以及如何優(yōu)化的討論。
二、使用方式
(1)給靜態(tài)方法加鎖
- public class Main {
- public static synchronized void staticSynPrint(String str) {
- System.out.println(str);
- }
- }
靜態(tài)方法不屬于任何一個實例,而是屬于該類。不管該類被實例化多少次,靜態(tài)成員只有一份。在同一時刻,不管是使用實例.staticSynPrint方式還是直接類名.staticSynPrint的方式,都會進(jìn)行同步處理。
(2)給靜態(tài)變量加鎖
同(1),他們都是該類的靜態(tài)成員。
(3)synchronized(xxx.class)
- public class Main {
- public void classSynPrint(String str) {
- synchronized (Main.class) {
- System.out.println(str);
- }
- }
- }
給當(dāng)前類加鎖(注意是當(dāng)前類,不是實例對象),會作用于該類的所有實例對象,多個線程訪問Main類中的所有同步方法,都需要先進(jìn)行同步處理。
(4)synchronized(this)
- public class Main {
- public void thisSynPrint(String str) {
- synchronized (this) {
- System.out.println(str);
- }
- }
- }
this代表實例對象,因此現(xiàn)在鎖住的是當(dāng)前實例對象,因此多個線程訪問不同實例的同步方法不需要進(jìn)行同步。
(5)給實例方法加鎖
- public class Main {
- public synchronized void synPrint(String str) {
- System.out.println(str);
- }
- }
不同線程訪問同一個實例底下的該方法,才會需要進(jìn)行同步。
三、實際使用方式之一:單例模式中的雙重檢驗鎖
更多單例模式的種類可以參考我的另外一篇博文【設(shè)計模式】單例模式
- public class SingletonDCL {
- private volatile static SingletonDCL instance;
- private SingletonDCL() {
- }
- public static SingletonDCL getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new SingletonDCL();
- }
- }
- }
- return instance;
- }
- }
有幾個疑問:
(1)這里為什么要檢驗兩次null?
最初的想法,是直接利用synchronized將整個getInstance方法鎖起來,但這樣效率太低,考慮到實際代碼更為復(fù)雜,我們應(yīng)當(dāng)縮小鎖的范圍。
在單例模式下,要的就是一個單例,new SingletonDCL()只能被執(zhí)行一次。因此,現(xiàn)在初步考慮成以下的這種方式:
- public static SingletonDCL getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- //一些耗時的操作
- instance = new SingletonDCL();
- }
- }
- return instance;
- }
但這樣,存在一個問題。線程1判斷instance為null,然后拿到鎖,執(zhí)行到了耗時的操作,阻塞了一會兒,還沒有對instance進(jìn)行實例化,instance還是為null。線程2判斷instance為null,嘗試去獲取鎖。線程1實例化instance之后,釋放了鎖。而線程2獲取鎖之后,同樣進(jìn)行了實例化操作。線程1和線程2拿到了兩個不同的對象,違背了單例的原則。
因此,在獲取鎖之后,又進(jìn)行了一次null檢驗。
(2)為什么使用volatile 修飾單例變量?
關(guān)于volatie和synchronized的區(qū)別,可以先參考我的另外一篇文章【JAVA】volatile和synchronized的區(qū)別
這段代碼,instance = new SingletonDCL(),在虛擬機(jī)層面,其實分為了3個指令:
為instance分配內(nèi)存空間,相當(dāng)于堆中開辟出來一段空間
實例化instance,相當(dāng)于在上一步開辟出來的空間上,放置實例化好的SingletonDCL對象
將instance變量引用指向第一步開辟出來的空間的首地址
但由于虛擬機(jī)做出的某些優(yōu)化,可能會導(dǎo)致指令重排序,由1->2->3變成1->3->2。這種重新排序在單線程下不會有任何問題,但出于多線程的情況下,可能會出現(xiàn)以下的問題:
線程1獲取鎖之后,執(zhí)行到了instance = new SingletonDCL()階段,此時,剛好由于虛擬機(jī)進(jìn)行了指令重排序,先進(jìn)行了第1步開辟內(nèi)存空間,然后執(zhí)行了第3步,instance指向空間首地址,第2步還沒來得及執(zhí)行,此時恰好有線程2執(zhí)行g(shù)etInstance方法,最外層判斷instance不為null(instance已經(jīng)指向了某一段地址,因此不為null),直接返回了單例對象,接著線程2在獲取單例對象屬性的時候,出現(xiàn)了空指針錯誤!
因此使用volatile 修飾單例變量,可以避免由于虛擬機(jī)的指令重排序機(jī)制可能導(dǎo)致的空指針異常。
四、實現(xiàn)原理
這里可以分兩種情況討論:
(1)同步語句塊
- public class Main {
- public static final Object object = new Object();
- public void print() {
- synchronized (object) {
- System.out.println("123");
- }
- }
- }
使用java Main.java,之后使用javap -c Main.class(-c代表反匯編)得到:
- public class com.yang.testSyn.Main {
- public static final java.lang.Object object;
- public com.yang.testSyn.Main();
- Code:
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: return
- public void print();
- Code:
- 0: getstatic #2 // Field object:Ljava/lang/Object;
- 3: dup
- 4: astore_1
- 5: monitorenter
- 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
- 9: ldc #4 // String 123
- 11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 14: aload_1
- 15: monitorexit
- 16: goto 24
- 19: astore_2
- 20: aload_1
- 21: monitorexit
- 22: aload_2
- 23: athrow
- 24: return
- Exception table:
- from to target type
- 6 16 19 any
- 19 22 19 any
- static {};
- Code:
- 0: new #6 // class java/lang/Object
- 3: dup
- 4: invokespecial #1 // Method java/lang/Object."<init>":()V
- 7: putstatic #2 // Field object:Ljava/lang/Object;
- 10: return
- }
其中print方法中的第5行、15行出現(xiàn)了monitorenter和monitorexit,而這兩行其中的字節(jié)碼代表的正是同步語句塊里的內(nèi)容。
當(dāng)線程執(zhí)行到monitorenter時,代表即將進(jìn)入到同步語句塊中,線程首先需要去獲得Object的對象鎖,而對象鎖處于每個java對象的對象頭中,對象頭中會有一個鎖的計數(shù)器,當(dāng)線程查詢對象頭中計數(shù)器,發(fā)現(xiàn)內(nèi)容為0時,則代表該對象沒有被任何線程所占有,此時該線程可以占有此對象,計數(shù)器于是加1。
線程占有該對象后,也就是拿到該對象的鎖,可以執(zhí)行同步語句塊里面的方法。此時,如果有其他線程進(jìn)來,查詢對象頭發(fā)現(xiàn)計數(shù)器不為0,于是進(jìn)入該對象的鎖等待隊列中,一直阻塞到計數(shù)器為0時,方可繼續(xù)執(zhí)行。
第一個線程執(zhí)行到enterexit后,釋放了Object的對象鎖,此時第二個線程可以繼續(xù)執(zhí)行。
這邊依然有幾個問題:
[1]為什么有一個monitorenter指令,卻有兩個monitorexit指令?
因為編譯器必須保證,無論同步代碼塊中的代碼以何種方式結(jié)束(正常 return 或者異常退出),代碼中每次調(diào)用 monitorenter 必須執(zhí)行對應(yīng)的 monitorexit 指令。為了保證這一點,編譯器會自動生成一個異常處理器,這個異常處理器的目的就是為了同步代碼塊拋出異常時能執(zhí)行 monitorexit。這也是字節(jié)碼中,只有一個 monitorenter 卻有兩個 monitorexit 的原因。
當(dāng)然這一點,也可以從Exception table(異常表)中看出來,字節(jié)碼中第6(from)到16(to)的偏移量中如果出現(xiàn)任何類型(type)的異常,都會跳轉(zhuǎn)到第19(target)行。
(2)同步方法
- public class Main {
- public synchronized void print(String str) {
- System.out.println(str);
- }
- }
使用javap -v Main.class查看
-v 選項可以顯示更加詳細(xì)的內(nèi)容,比如版本號、類訪問權(quán)限、常量池相關(guān)的信息,是一個非常有用的參數(shù)。
- public class com.yang.testSyn.Main
- minor version: 0
- major version: 52
- flags: ACC_PUBLIC, ACC_SUPER
- Constant pool:
- #1 = Methodref #5.#14 // java/lang/Object."<init>":()V
- #2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
- #3 = Methodref #17.#18 // java/io/PrintStream.println:(Ljava/lang/String;)V
- #4 = Class #19 // com/yang/testSyn/Main
- #5 = Class #20 // java/lang/Object
- #6 = Utf8 <init>
- #7 = Utf8 ()V
- #8 = Utf8 Code
- #9 = Utf8 LineNumberTable
- #10 = Utf8 print
- #11 = Utf8 (Ljava/lang/String;)V
- #12 = Utf8 SourceFile
- #13 = Utf8 Main.java
- #14 = NameAndType #6:#7 // "<init>":()V
- #15 = Class #21 // java/lang/System
- #16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
- #17 = Class #24 // java/io/PrintStream
- #18 = NameAndType #25:#11 // println:(Ljava/lang/String;)V
- #19 = Utf8 com/yang/testSyn/Main
- #20 = Utf8 java/lang/Object
- #21 = Utf8 java/lang/System
- #22 = Utf8 out
- #23 = Utf8 Ljava/io/PrintStream;
- #24 = Utf8 java/io/PrintStream
- #25 = Utf8 println
- {
- public com.yang.testSyn.Main();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: return
- LineNumberTable:
- line 3: 0
- public synchronized void print(java.lang.String);
- descriptor: (Ljava/lang/String;)V
- flags: ACC_PUBLIC, ACC_SYNCHRONIZED
- Code:
- stack=2, locals=2, args_size=2
- 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
- 3: aload_1
- 4: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 7: return
- LineNumberTable:
- line 32: 0
- line 33: 7
- }
只看最后兩個方法,第一個方法是編譯后自動生成的默認(rèn)構(gòu)造方法,第二個方法則是我們的同步方法,可以看到同步方法比默認(rèn)的構(gòu)造方法多了一個ACC_SYNCHRONIZED的標(biāo)志位。
與同步語句塊不同,虛擬機(jī)不會在字節(jié)碼層面實現(xiàn)鎖同步,而是會先觀察該方法是否含有ACC_SYNCHRONIZED標(biāo)志。如果含有,則線程會首先嘗試獲取鎖。如果是實例方法,則會嘗試獲取實例鎖;如果是靜態(tài)方法(類方法),則會嘗試獲取類鎖。最后不管方法執(zhí)行是否出現(xiàn)異常,都會釋放鎖。