一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - 淺說Synchronized的底層實現原理

淺說Synchronized的底層實現原理

2021-01-08 23:26java小當家 Java教程

synchronized關鍵字用來保證在同一時刻只有一個線程可以執行被它修飾的變量或者代碼塊。

 一、前言

synchronized關鍵字用來保證在同一時刻只有一個線程可以執行被它修飾的變量或者代碼塊。

這一篇中,只涉及synchronized的底層實現原理,不涉及對synchronized效率以及如何優化的討論。

二、使用方式

(1)給靜態方法加鎖

public class Main { 

    

    public static synchronized void staticSynPrint(String str) { 

        System.out.println(str); 

    } 

 

靜態方法不屬于任何一個實例,而是屬于該類。不管該類被實例化多少次,靜態成員只有一份。在同一時刻,不管是使用實例.staticSynPrint方式還是直接類名.staticSynPrint的方式,都會進行同步處理。

(2)給靜態變量加鎖

同(1),他們都是該類的靜態成員。

(3)synchronized(xxx.class)

public class Main { 

 

    public void classSynPrint(String str) { 

        synchronized (Main.class) { 

            System.out.println(str); 

        } 

    } 

 

給當前類加鎖(注意是當前類,不是實例對象),會作用于該類的所有實例對象,多個線程訪問Main類中的所有同步方法,都需要先進行同步處理。

(4)synchronized(this)

public class Main { 

 

    public void thisSynPrint(String str) { 

        synchronized (this) { 

            System.out.println(str); 

        } 

    } 

 

this代表實例對象,因此現在鎖住的是當前實例對象,因此多個線程訪問不同實例的同步方法不需要進行同步。

(5)給實例方法加鎖

public class Main { 

 

    public synchronized void synPrint(String str) { 

        System.out.println(str); 

    } 

 

不同線程訪問同一個實例底下的該方法,才會需要進行同步。

三、實際使用方式之一:單例模式中的雙重檢驗鎖

更多單例模式的種類可以參考我的另外一篇博文【設計模式】單例模式

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方法鎖起來,但這樣效率太低,考慮到實際代碼更為復雜,我們應當縮小鎖的范圍。

在單例模式下,要的就是一個單例,new SingletonDCL()只能被執行一次。因此,現在初步考慮成以下的這種方式:

public static SingletonDCL getInstance() { 

       if (instance == null) { 

           synchronized (Singleton.class) { 

                   //一些耗時的操作 

                   instance = new SingletonDCL(); 

           } 

       } 

       return instance; 

   } 

但這樣,存在一個問題。線程1判斷instance為null,然后拿到鎖,執行到了耗時的操作,阻塞了一會兒,還沒有對instance進行實例化,instance還是為null。線程2判斷instance為null,嘗試去獲取鎖。線程1實例化instance之后,釋放了鎖。而線程2獲取鎖之后,同樣進行了實例化操作。線程1和線程2拿到了兩個不同的對象,違背了單例的原則。

因此,在獲取鎖之后,又進行了一次null檢驗。

(2)為什么使用volatile 修飾單例變量?

關于volatie和synchronized的區別,可以先參考我的另外一篇文章【JAVA】volatile和synchronized的區別

這段代碼,instance = new SingletonDCL(),在虛擬機層面,其實分為了3個指令:

為instance分配內存空間,相當于堆中開辟出來一段空間

實例化instance,相當于在上一步開辟出來的空間上,放置實例化好的SingletonDCL對象

將instance變量引用指向第一步開辟出來的空間的首地址

但由于虛擬機做出的某些優化,可能會導致指令重排序,由1->2->3變成1->3->2。這種重新排序在單線程下不會有任何問題,但出于多線程的情況下,可能會出現以下的問題:

線程1獲取鎖之后,執行到了instance = new SingletonDCL()階段,此時,剛好由于虛擬機進行了指令重排序,先進行了第1步開辟內存空間,然后執行了第3步,instance指向空間首地址,第2步還沒來得及執行,此時恰好有線程2執行getInstance方法,最外層判斷instance不為null(instance已經指向了某一段地址,因此不為null),直接返回了單例對象,接著線程2在獲取單例對象屬性的時候,出現了空指針錯誤!

因此使用volatile 修飾單例變量,可以避免由于虛擬機的指令重排序機制可能導致的空指針異常。

四、實現原理

這里可以分兩種情況討論:

(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行出現了monitorenter和monitorexit,而這兩行其中的字節碼代表的正是同步語句塊里的內容。

當線程執行到monitorenter時,代表即將進入到同步語句塊中,線程首先需要去獲得Object的對象鎖,而對象鎖處于每個java對象的對象頭中,對象頭中會有一個鎖的計數器,當線程查詢對象頭中計數器,發現內容為0時,則代表該對象沒有被任何線程所占有,此時該線程可以占有此對象,計數器于是加1。

線程占有該對象后,也就是拿到該對象的鎖,可以執行同步語句塊里面的方法。此時,如果有其他線程進來,查詢對象頭發現計數器不為0,于是進入該對象的鎖等待隊列中,一直阻塞到計數器為0時,方可繼續執行。

第一個線程執行到enterexit后,釋放了Object的對象鎖,此時第二個線程可以繼續執行。

這邊依然有幾個問題:

[1]為什么有一個monitorenter指令,卻有兩個monitorexit指令?

因為編譯器必須保證,無論同步代碼塊中的代碼以何種方式結束(正常 return 或者異常退出),代碼中每次調用 monitorenter 必須執行對應的 monitorexit 指令。為了保證這一點,編譯器會自動生成一個異常處理器,這個異常處理器的目的就是為了同步代碼塊拋出異常時能執行 monitorexit。這也是字節碼中,只有一個 monitorenter 卻有兩個 monitorexit 的原因。

當然這一點,也可以從Exception table(異常表)中看出來,字節碼中第6(from)到16(to)的偏移量中如果出現任何類型(type)的異常,都會跳轉到第19(target)行。

(2)同步方法

public class Main { 

 

    public synchronized void print(String str) { 

        System.out.println(str); 

    } 

 

使用javap -v Main.class查看

-v 選項可以顯示更加詳細的內容,比如版本號、類訪問權限、常量池相關的信息,是一個非常有用的參數。

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 

只看最后兩個方法,第一個方法是編譯后自動生成的默認構造方法,第二個方法則是我們的同步方法,可以看到同步方法比默認的構造方法多了一個ACC_SYNCHRONIZED的標志位。

與同步語句塊不同,虛擬機不會在字節碼層面實現鎖同步,而是會先觀察該方法是否含有ACC_SYNCHRONIZED標志。如果含有,則線程會首先嘗試獲取鎖。如果是實例方法,則會嘗試獲取實例鎖;如果是靜態方法(類方法),則會嘗試獲取類鎖。最后不管方法執行是否出現異常,都會釋放鎖。

 

原文地址:https://www.toutiao.com/i6914914346254533123/

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 亚洲精品在看在线观看 | 7777奇米四色 | 色婷婷综合久久久中文字幕 | 91精品免费国产高清在线 | 天海翼黄色三级 | 兽皇videos日本另类 | 国产偷啪 | 小小水蜜桃视频高清在线播放 | 秋霞理论一级在线观看手机版 | 精品在线免费观看视频 | 精品亚洲视频在线 | 国产亚洲小视频 | 视频免费在线 | 日本精品一区二区在线播放 | 国内精品久久久久小说网 | 7777色鬼xxxx欧美色夫 | 校园春色偷拍自拍 | 四虎影院在线免费观看视频 | 蜜桃成熟时1997在线看免费看 | 日韩成本大片35分钟免费播放 | 青草福利视频 | 午夜一区二区三区 | 99久久国产视频 | 亚洲成人国产 | 草莓香蕉榴莲丝瓜秋葵绿巨人在线看 | 国产高清自拍 | 精品视频手机在线观看免费 | 草莓丝瓜芭乐樱桃榴莲色多黄 | 久久国产精品永久免费网站 | 日本一区二区不卡久久入口 | 天天白天天谢天天啦 | 国产免费一区二区 | 别停好爽好深好大好舒服视频 | eeuss免费快捷 | 四虎影院在线免费观看视频 | 乌克兰肛交影视 | 高清欧美不卡一区二区三区 | 国产高清在线不卡 | 久久精品国产免费 | 福利视频一区二区思瑞 | 亚洲欧美日韩精品高清 |