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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

面试官:说一下Synchronized底层实现,锁升级的具体过程?

發布時間:2023/12/8 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 面试官:说一下Synchronized底层实现,锁升级的具体过程? 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

介紹

這是我去年7,8月份面試的時候被問的一個面試題,說實話被問到這個問題還是很意外的,感覺這個東西沒啥用啊,直到后面被問了一波new Object,Integer對象等作為加鎖對象行嗎?會出現哪些問題?為啥java6后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字節的整數倍。填充數據不是必須存在的,僅僅是為了字節對齊

    假如有如下的類,a=100這個信息就存儲在實例變量中

    public class Test {int a = 100; }

    填充數據主要是為了方便內存管理,如你想要10字節的內存,但是會給你分配16字節的內存,多出來的字節就是填充數據

    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 ; //等待獲取鎖的線程,會被加入到該列表 }


    結合線程狀態解釋一下執行過程。(狀態裝換參考自《深入理解Java虛擬機》)

  • 新建(New),新建后尚未啟動的線程
  • 運行(Runable),Runnable包括了操作系統線程狀態中的Running和Ready
  • 無限期等待(Waiting),不會被分配CPU執行時間,要等待被其他線程顯式的喚醒。例如調用沒有設置Timeout參數的Object.wait()方法
  • 限期等待(Timed Waiting),不會被分配CPU執行時間,不過無需等待其他線程顯示的喚醒,在一定時間之后會由系統自動喚醒。例如調用Thread.sleep()方法
  • 阻塞(Blocked),線程被阻塞了,“阻塞狀態”與“等待狀態”的區別是:“阻塞狀態”在等待獲取著一個排他鎖,這個事件將在另外一個線程放棄這個鎖的時候發生,而“等待狀態”則是在等待一段時間,或者喚醒動作的發生。在程序等待進入同步區域的時候,線程將進入這種狀態
  • 結束(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是通過什么方式來獲取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,表明這是一個同步方法

    鎖的升級

    在Java早期版本中,synchronized屬于重量級鎖,效率低下,因為操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高。

    慶幸的是在jdk1.6之后Java官方對從JVM層面對synchronized較大優化,所以現在的synchronized鎖效率也優化得很不錯了,Jdk1.6之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了偏向鎖和輕量級鎖,簡單介紹一下

    jdk1.6之前的synchronized到底有多慢?

    我們假設執行下面的代碼用的是jdk1.5,來看看會發生什么。
    假設doSomeThing執行1000次,才有可能發生一次并發執行。但是每次都需要讓操作系統從用戶態轉換到核心態,太耗時了。

    public class RunTest {public synchronized void doSomeThing() {} }

    然后Doug Lea看不下去了(你用的并發包就是他寫的),寫了ReentrantLock類,效率比synchronized快多了,為了理解讓大家理解ReentrantLock到底快在哪?我仿造ReentrantLock寫一個實現

    public class MyLock {private volatile int state;// 這里應該用并發安全的容器,這里只是舉例private List<Thread> threadList = new ArrayList<>();private static final Unsafe unsafe;private static final long stateOffset;static {try {Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);unsafe = (Unsafe) field.get(null);stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("state"));} catch (Exception ex) { throw new Error(ex); }}public void lock() {while (!compareAndSetState(0, 1)) {park();}}public void unLock() {while (compareAndSetState(1, 0)) {unPark();}}private void park() {threadList.add(Thread.currentThread());LockSupport.park(Thread.currentThread());}private void unPark() {if (!threadList.isEmpty()) {Thread thread = threadList.get(0);System.out.println(thread.getName());LockSupport.unpark(thread);}}private boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update);} }

    可以看到在api層面就已經解決并發問題,加鎖沒有競爭的時候一個cas就搞定了,節省了大量時間

    Doug Lea一個類的效率都比synchronized的效率高,估計synchronized的開發人員看了都不好意思了,于是對synchronized進行了一系列改造,即我們常說的鎖升級過程。

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

    偏向鎖

    為什么要引入偏向鎖?

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

    偏向鎖原理和升級過程

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

    輕量級鎖

    為什么要引入輕量級鎖?

    輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因為阻塞線程需要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空轉。

    幾種鎖的優缺點

    用鎖的最佳實踐

    錯誤的加鎖姿勢1

    synchronized (new Object())

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

    錯誤的加鎖姿勢2

    private Integer count; synchronized (count)

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

    Java常量池詳解,秒懂各種對象相等操作

    正確的加鎖姿勢

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

    題外話

    ConcurrentHashMap在jdk1.7的時候,實現用的是分段鎖,用ReentrantLock來保證并發安全。
    而在jdk1.8的時候,拋棄了原有的分段鎖,而采用了 CAS + synchronized 來保證并發安全性,也可以說明synchronized的的效率現在確實很高了。

    歡迎關注

    參考博客

    好文
    [0]https://blog.csdn.net/tongdanping/article/details/79647337
    [1]https://blog.csdn.net/javazejian/article/details/72828483
    [2]https://www.cnblogs.com/grow001/p/12232708.html?utm_source=gold_browser_extension
    [3]https://blog.csdn.net/chenssy/article/details/54883355
    lock接口方法簡介
    [4]https://zhuanlan.zhihu.com/p/38264728
    condition的使用
    [5]https://www.cnblogs.com/xrq730/p/4855155.html
    線程狀態
    [6]https://blog.csdn.net/xiaosheng900523/article/details/82964768

    總結

    以上是生活随笔為你收集整理的面试官:说一下Synchronized底层实现,锁升级的具体过程?的全部內容,希望文章能夠幫你解決所遇到的問題。

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