日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

synchronized的底层原理

發布時間:2024/9/30 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 synchronized的底层原理 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

synchronized使用方式

我們知道并發編程會產生各種問題的源頭是可見性,原子性,有序性。
而synchronized能同時保證可見性,原子性,有序性。所以我們在解決并發問題的時候經常用synchronized,當然還有很多其他工具,如volatile。但是volatile只能保證可見性,有序性,不能保證原子性。參見之前的文章volatile關鍵字——保證并發編程中的可見性、有序性

synchronized可以用在如下地方

  • 修飾實例方法,對當前實例對象this加鎖
  • 修飾靜態方法,對當前類的Class對象加鎖
  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖
  • 修飾實例方法

    public class SynchronizedDemo {public synchronized void methodOne() {} }

    修飾靜態方法

    public class SynchronizedDemo {public static synchronized void methodTwo() {} }

    修飾代碼塊

    public class SynchronizedDemo {public void methodThree() {// 對當前實例對象this加鎖synchronized (this) {}}public void methodFour() {// 對class對象加鎖synchronized (SynchronizedDemo.class) {}} }

    synchronized實現原理

    Java對象組成

    我們都知道對象是放在堆內存中的,對象大致可以分為三個部分,分別是對象頭,實例變量和填充字節

  • 對象頭,主要包括兩部分1. Mark Word (標記字段),2.Klass Pointer(類型指針)。Klass Point 是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。Mark Word用于存儲對象自身的運行時數據
  • 實例變量,存放類的屬性數據信息,包括父類的屬性信息,這部分內存按4字節對齊
  • 填充數據,由于虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是為了字節對齊
  • synchronized不論是修飾方法還是代碼塊,都是通過持有修飾對象的鎖來實現同步,那么synchronized鎖對象是存在哪里的呢?答案是存在鎖對象的對象頭Mark Word,來看一下Mark Word存儲了哪些內容?

    由于對象頭的信息是與對象自身定義的數據沒有關系的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設計成為一個非固定的數據結構,以便存儲更多有效的數據,它會根據對象本身的狀態復用自己的存儲空間,也就是說,Mark Word會隨著程序的運行發生變化,變化狀態如下 (32位虛擬機):

    其中輕量級鎖和偏向鎖是Java 6 對 synchronized 鎖進行優化后新增加的,稍后我們會簡要分析。這里我們主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象(也稱為管程或監視器鎖)的起始地址。每個對象都存在著一個 monitor 與之關聯。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的),省略部分屬性

    ObjectMonitor() {_count = 0; //記錄數_recursions = 0; //鎖的重入次數_owner = NULL; //指向持有ObjectMonitor對象的線程 _WaitSet = NULL; //調用wait后,線程會被加入到_WaitSet_EntryList = NULL ; //等待獲取鎖的線程,會被加入到該列表 }


    注意這里的_entrylist和WaitSet,wait線程是在一個set里,這會不會notify隨機喚醒對應起來。

    結合線程狀態解釋一下執行過程。

  • 新建(New):新建后尚未啟動的線程
  • 阻塞(Blocked):線程被阻塞,等待著獲取monitor lock進入同步代碼塊或同步方法。“阻塞狀態”與“等待狀態”的區別是:“阻塞狀態”在等待獲取著一個排他鎖,這個事件將在另外一個線程放棄這個鎖的時候發生
  • 運行(Runable):Runnable包括了操作系統線程狀態中的Running和Ready
  • 無限期等待(Waiting):不會被分配CPU執行時間,進入等待狀態的線程要等待被其他線程顯式的喚醒比如notify()或notifyAll()。調用沒有設置Timeout參數的Object.wait()方法、沒有設置超時時間的Thread.join()方法、ockSupport.park()方法會使線程進入Waiting狀態。
  • 限期等待(Timed Waiting):不會被分配CPU執行時間,不過無需等待其他線程顯示的喚醒,在一定時間之后會由系統自動喚醒。例如調用Thread.sleep()方法,設置超時時間的Object.wait(long、Thread.join(long) 等
  • 結束(Terminated):線程結束執行
  • 對于一個synchronized修飾的方法(代碼塊)來說:

  • 當多個線程同時訪問該方法,那么這些線程會先被放進_EntryList隊列,此時線程處于blocked狀態
  • 當一個線程獲取到了對象的monitor后,那么就可以進入running狀態,執行方法。此時,ObjectMonitor對象的/_owner指向當前線程,_count加1表示當前對象鎖被一個線程獲取
  • 當running狀態的線程調用wait()方法,那么當前線程釋放monitor對象,進入waiting狀態,ObjectMonitor對象的/_owner變為null,_count減1,同時線程進入_WaitSet隊列,直到有線程調用notify()方法喚醒該線程,則該線程進入_EntryList隊列,競爭到鎖再進入_Owner區
  • 如果當前線程執行完畢,那么也釋放monitor對象,ObjectMonitor對象的/_owner變為null,_count減1
  • 由此看來,monitor對象存在于每個Java對象的對象頭中(存儲的是指針),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因

    synchronized如何獲取monitor對象?

    synchronized修飾代碼塊

    public class SyncCodeBlock {public int count = 0;public void addOne() {synchronized (this) {count++;}} } javac SyncCodeBlock.java javap -v SyncCodeBlock.class

    反編譯的字節碼如下

    public void addOne();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter // 進入同步方法4: aload_05: dup6: getfield #2 // Field count:I9: iconst_110: iadd11: putfield #2 // Field count:I14: aload_115: monitorexit // 退出同步方法16: goto 2419: astore_220: aload_121: monitorexit // 退出同步方法22: aload_223: athrow24: returnException table:

    可以看到進入同步代碼塊,執行monitorenter指令,退出同步代碼塊,執行monitorexit指令,可以看到有2個monitorexit指令,第一個是正常退出執行的,第二個是當異常發生時執行的

    synchronized修飾方法

    public class SyncMethod {public int count = 0;public synchronized void addOne() {count++;} }

    反編譯的字節碼如下

    public synchronized void addOne();descriptor: ()V// 方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法flags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field count:I5: iconst_16: iadd7: putfield #2 // Field count:I10: returnLineNumberTable:

    我們并沒有看到monitorenter和monitorexit指令,那是怎么來實現同步的呢?
    可以看到方法被標識為ACC_SYNCHRONIZED,表明這是一個同步方法

    synchronized鎖的升級

    在Java早期版本中,synchronized屬于重量級鎖,效率低下,因為操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高。慶幸的是在Java 6之后Java官方對從JVM層面對synchronized較大優化,所以現在的synchronized鎖效率也優化得很不錯了,Java 6之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了偏向鎖和輕量級鎖,簡單介紹一下

    synchronized鎖有四種狀態,無鎖,偏向鎖,輕量級鎖,重量級鎖,這幾個狀態會隨著競爭狀態逐漸升級,鎖可以升級但不能降級,但是偏向鎖狀態可以被重置為無鎖狀態

    偏向鎖

    為什么要引入偏向鎖?

    因為經過HotSpot的作者大量的研究發現,大多數時候是不存在鎖競爭的,常常是一個線程多次獲得同一個鎖,因此如果每次都要競爭鎖會增大很多沒有必要付出的代價,為了降低獲取鎖的代價,才引入的偏向鎖。

    偏向鎖原理和升級過程

    當線程1訪問代碼塊并獲取鎖對象時,會在java對象頭和棧幀中記錄偏向的鎖的threadID,因為偏向鎖不會主動釋放鎖,因此以后線程1再次獲取鎖的時候,需要比較當前線程的threadID和Java對象頭中的threadID是否一致,如果一致(還是線程1獲取鎖對象),則無需使用CAS來加鎖、解鎖;如果不一致(其他線程,如線程2要競爭鎖對象,而偏向鎖不會主動釋放因此還是存儲的線程1的threadID),那么需要查看Java對象頭中記錄的線程1是否存活,如果線程1存活且仍需要持有這個鎖,那么暫停當前線程1,撤銷偏向鎖,升級為輕量級鎖。如果線程1沒有存活或者不再需要這個鎖,那么鎖對象被重置為無鎖狀態,其它線程(線程2)可以競爭將其設置為偏向鎖。

    輕量級鎖

    為什么要引入輕量級鎖?

    輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因為阻塞線程需要CPU從用戶態轉到內核態,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就干脆不阻塞這個線程,讓它自旋這等待鎖釋放。

    輕量級鎖原理和升級過程

    線程1獲取輕量級鎖時會先把鎖對象的對象頭MarkWord復制一份到線程1的棧幀中創建的用于存儲鎖記錄的空間(稱為DisplacedMarkWord),然后使用CAS把對象頭中的內容替換為線程1存儲的鎖記錄(DisplacedMarkWord)的地址;

    如果在線程1復制對象頭的同時(在線程1CAS之前),線程2也準備獲取鎖,復制了對象頭到線程2的鎖記錄空間中,但是在線程2CAS的時候,發現線程1已經把對象頭換了,線程2的CAS失敗,那么線程2就嘗試使用自旋鎖來等待線程1釋放鎖。 自旋鎖簡單來說就是讓線程2在循環中不斷CAS,但是如果自旋的時間太長也不行,因為自旋是要消耗CPU的,因此自旋的次數是有限制的,比如10次或者100次。如果自旋次數到了線程1還沒有釋放鎖,或者線程1還在執行,線程2還在自旋等待,這時又有一個線程3過來競爭這個鎖對象,那么這個時候輕量級鎖就會膨脹為重量級鎖。重量級鎖把除了擁有鎖的線程都阻塞,防止CPU空轉。

    幾種鎖的優缺點

    synchronized鎖的最佳實踐

    • 錯誤的加鎖姿勢1

      synchronized (new Object())

    每次調用創建的是不同的鎖,相當于無鎖

    • 錯誤的加鎖姿勢2

      private Integer count; synchronized (count)

      String,Integer 都用了享元模式,即值在一定范圍內對象是同一個。所以看似是用了不同的對象,其實用的是同一個對象。會導致一個鎖被多個地方使用

    • 正確的加鎖姿勢

      // 普通對象鎖 private final Object lock = new Object(); // 靜態對象鎖 private static final Object lock = new Object();

    最后總結一波,synchronized和ReentrantLock的異同

    既然有了synchronized,為啥還要提供Lock接口呢?也許你會說Lock接口比synchronized性能高。在jdk1.5之前確實如此,但是在jdk1.6之后,兩者性能差不多了。

  • ReentrantLock支持的功能更多,如支持非阻塞的方式獲取鎖,能夠響應中斷等,而synchronized不行;ReentrantLock可以是公平鎖或者非公平鎖,而synchronized只能是非公平鎖
  • ReentrantLock需要手動顯示的獲取和釋放鎖,一定記得在finally里手動釋放鎖,防止發生異常時沒有釋放鎖;而synchronized會隱式的釋放鎖,發生異常也會自動釋放鎖,不會導致死鎖的發生
  • synchronized和ReentrantLock都是可重入鎖
  • 與50位技術專家面對面20年技術見證,附贈技術全景圖

    總結

    以上是生活随笔為你收集整理的synchronized的底层原理的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。