Java面試之Synchronized解析

參考:https://juejin.im/post/5d5374076fb9a06ac76da894#heading-8
synchronized,是解決併發情況下數據同步訪問問題的一把利刃,也是面試經常聞到的一個知識點。那麼synchronized的底層原理是什麼呢?

Synchronized的使用場景

synchronized關鍵字可以作用於方法或者代碼塊,最主要有以下幾種使用方式,如圖:
在這裏插入圖片描述
接下來,我們先看synchronized的第一層,反編譯其作用的代碼塊以及方法。
synchronized作用於代碼塊

public class SynchronizedTest {

    public void doSth(){
        synchronized (SynchronizedTest.class){
            System.out.println("test Synchronized" );
        }
    }
}

反編譯,可得:
在這裏插入圖片描述
由圖可得,添加了synchronized關鍵字的代碼塊,多了兩個指令monitorenter、monitorexit。即JVM使用monitorenter和monitorexit兩個指令實現同步,monitorenter、monitorexit又是怎樣保證同步的呢?我們等下剝第二層繼續探索。
synchronized作用於方法

 public synchronized void doSth(){
            System.out.println("test Synchronized method" );
    }

反編譯,可得:
在這裏插入圖片描述
由圖可得,添加了synchronized關鍵字的方法,多了ACC_SYNCHRONIZED標記。即JVM通過在方法訪問標識符(flags)中加入ACC_SYNCHRONIZED來實現同步功能。

monitorenter、monitorexit、ACC_SYNCHRONIZED

反編譯synchronized的方法以及代碼塊,我們已經知道synchronized是通過monitorenter、monitorexit、ACC_SYNCHRONIZED實現同步的,它們三作用都是啥呢?我們接着深入:
monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

谷歌翻譯一下,如下:

每個對象都與一個monitor 相關聯。當且僅當擁有所有者時(被擁有),monitor纔會被鎖定。執行到monitorenter指令的線程,會嘗試去獲得對應的monitor,如下:

每個對象維護着一個記錄着被鎖次數的計數器, 對象未被鎖定時,該計數器爲0。線程進入monitor(執行monitorenter指令)時,會把計數器設置爲1.
當同一個線程再次獲得該對象的鎖的時候,計數器再次自增.
當其他線程想獲得該monitor的時候,就會阻塞,直到計數器爲0才能成功。

可以看一下以下的圖,便於理解用:
在這裏插入圖片描述
monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

谷歌翻譯一下,如下:

monitor的擁有者線程才能執行 monitorexit指令。

線程執行monitorexit指令,就會讓monitor的計數器減一。如果計數器爲0,表明該線程不再擁有monitor。其他線程就允許嘗試去獲得該monitor了。

可以看一下以下的圖,便於理解用:
在這裏插入圖片描述
ACC_SYNCHRONIZED

Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

谷歌翻譯一下,如下:

方法級別的同步是隱式的,作爲方法調用的一部分。同步方法的常量池中會有一個ACC_SYNCHRONIZED標誌。
當調用一個設置了ACC_SYNCHRONIZED標誌的方法,執行線程需要先獲得monitor鎖,然後開始執行方法,方法執行之後再釋放monitor鎖,當方法不管是正常return還是拋出異常都會釋放對應的monitor鎖。
在這期間,如果其他線程來請求執行方法,會因爲無法獲得監視器鎖而被阻斷住。
如果在方法執行過程中,發生了異常,並且方法內部並沒有處理該異常,那麼在異常被拋到方法外面之前監視器鎖會被自動釋放。

可以看一下這個流程圖:
在這裏插入圖片描述
Synchronized第二層的總結

  • 同步代碼塊是通過monitorenter和monitorexit來實現,當線程執行到monitorenter的時候要先獲得monitor鎖,才能執行後面的方法。當線程執行到monitorexit的時候則要釋放鎖。
  • 同步方法是通過中設置ACC_SYNCHRONIZED標誌來實現,當線程執行有ACC_SYNCHRONI標誌的方法,需要獲得monitor鎖。
  • 每個對象維護一個加鎖計數器,爲0表示可以被其他線程獲得鎖,不爲0時,只有當前鎖的線程才能再次獲得鎖。
  • 同步方法和同步代碼塊底層都是通過monitor來實現同步的。
  • 每個對象都與一個monitor相關聯,線程可以佔有或者釋放monitor。

好的,到這裏,我們還有一些不清楚的地方,monitor是什麼呢,爲什麼它可以實現同步呢?對象又是怎樣跟monitor關聯的呢?客觀別急,我們繼續剝下一層,請往下看。

monitor監視器

monitor到底是什麼呢?我們接下來看Synchronized的第三層,monitor是什麼? 它可以理解爲一種同步工具,或者說是同步機制,它通常被描述成一個對象。操作系統的管程是概念原理,ObjectMonitor是它的原理實現。
在這裏插入圖片描述
操作系統的管程

  • 管程 (英語:Monitors,也稱爲監視器) 是一種程序結構,結構內的多個子程序(對象或模塊)形成的多個工作線程互斥訪問共享資源。
  • 這些共享資源一般是硬件設備或一羣變量。管程實現了在一個時間點,最多隻有一個線程在執行管程的某個子程序。
  • 與那些通過修改數據結構實現互斥訪問的併發程序設計相比,管程實現很大程度上簡化了程序設計。
  • 管程提供了一種機制,線程可以臨時放棄互斥訪問,等待某些條件得到滿足後,重新獲得執行權恢復它的互斥訪問。
    ObjectMonitor
    ObjectMonitor數據結構
    在Java虛擬機(HotSpot)中,Monitor(管程)是由ObjectMonitor實現的,其主要數據結構如下:
 ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;  // 處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor關鍵字
ObjectMonitor中幾個關鍵字段的含義如圖所示:
在這裏插入圖片描述
工作機理
Java Monitor 的工作機理如圖所示:

在這裏插入圖片描述

  • 想要獲取monitor的線程,首先會進入_EntryList隊列。
  • 當某個線程獲取到對象的monitor後,進入_Owner區域,設置爲當前線程,同時計數器_count加1。
  • 如果線程調用了wait()方法,則會進入_WaitSet隊列。它會釋放monitor鎖,即將_owner賦值爲null,_count自減1,進入_WaitSet隊列阻塞等待。
  • 如果其他線程調用 notify() / notifyAll()
    ,會喚醒_WaitSet中的某個線程,該線程再次嘗試獲取monitor鎖,成功即進入_Owner區域。
  • 同步方法執行完畢了,線程退出臨界區,會將monitor的owner設爲null,並釋放監視鎖。

爲了形象生動一點,舉個例子:

  synchronized(this){  //進入_EntryList隊列
            doSth();
            this.wait();  //進入_WaitSet隊列
        }

OK,我們知道了monitor是什麼了,那麼對象又是怎樣跟monitor關聯呢?各位,我們接着往下看,去剝下一層。

對象與monitor關聯

對象是如何跟monitor關聯的呢?直接先看圖:
在這裏插入圖片描述
看完上圖,其實對象跟monitor怎樣關聯,我們已經有個大概認識了,接下來我們分對象內存佈局,對象頭,MarkWord一層層繼續往下探討。
對象的內存佈局
在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header),實例數據(Instance Data)和對象填充(Padding)。
在這裏插入圖片描述

  • 實例數據: 對象真正存儲的有效信息,存放類的屬性數據信息,包括父類的屬性信息;
  • 對齊填充: 由於虛擬機要求 對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊。
  • 對象頭: Hotspot虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Class Pointer(類型指針)。

對象頭
對象頭主要包括兩部分數據:Mark Word(標記字段)、Class Pointer(類型指針)。
在這裏插入圖片描述
Mark word
Mark Word 用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等。
在32位的HotSpot虛擬機中,如果對象處於未被鎖定的狀態下,那麼Mark Word的32bit空間裏的25位用於存儲對象哈希碼,4bit用於存儲對象分代年齡,2bit用於存儲鎖標誌位,1bit固定爲0,表示非偏向鎖。其他狀態如下圖所示:
在這裏插入圖片描述

  • 前面分析可知,monitor特點是互斥進行,你再喵一下上圖,重量級鎖,指向互斥量的指針。
  • 其實synchronized是重量級鎖,也就是說Synchronized的對象鎖,Mark
    Word鎖標識位爲10,其中指針指向的是Monitor對象的起始地址。
  • 頓時,是不是感覺柳暗花明又一村啦!對象與monitor怎麼關聯的?答案:Mark Word重量級鎖,指針指向monitor地址。

Synchronized第四層小總結
對象與monitor怎麼關聯?

  • 對象裏有對象頭
  • 對象頭裏面有Mark Word
  • Mark Word指針指向了monitor

鎖優化

事實上,只有在JDK1.6之前,synchronized的實現纔會直接調用ObjectMonitor的enter和exit,這種鎖被稱之爲重量級鎖。一個重量級鎖,爲啥還要經常使用它呢? 從JDK6開始,HotSpot虛擬機開發團隊對Java中的鎖進行優化,如增加了適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等優化策略。

自旋鎖
何爲自旋鎖?

自旋鎖是指當一個線程嘗試獲取某個鎖時,如果該鎖已被其他線程佔用,就一直循環檢測鎖是否被釋放,而不是進入線程掛起或睡眠狀態。
爲何需要自旋鎖?
線程的阻塞和喚醒需要CPU從用戶態轉爲核心態,頻繁的阻塞和喚醒顯然對CPU來說苦不吭言。其實很多時候,鎖狀態只持續很短一段時間,爲了這段短暫的光陰,頻繁去阻塞和喚醒線程肯定不值得。因此自旋鎖應運而生。
自旋鎖應用場景
自旋鎖適用於鎖保護的臨界區很小的情況,臨界區很小的話,鎖佔用的時間就很短。
自旋鎖一些思考
在這裏,我想談談,爲什麼ConcurrentHashMap放棄分段鎖,而使用CAS自旋方式,其實也是這個道理。
鎖消除
何爲鎖消除?

鎖削除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行削除。
鎖消除一些思考
在這裏,我想引申到日常代碼開發中,有一些開發者,在沒併發情況下,也使用加鎖。如沒併發可能,直接上來就ConcurrentHashMap。
鎖粗化
何爲鎖租化?

鎖粗話概念比較好理解,就是將多個連續的加鎖、解鎖操作連接在一起,擴展成一個範圍更大的鎖。
爲何需要鎖租化?
在使用同步鎖的時候,需要讓同步塊的作用範圍儘可能小—僅在共享數據的實際作用域中才進行同步,這樣做的目的是 爲了使需要同步的操作數量儘可能縮小,如果存在鎖競爭,那麼等待鎖的線程也能儘快拿到鎖。但是如果一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗,所以引入鎖粗話的概念。
鎖租化比喻思考
舉個例子,買門票進動物園。老師帶一羣小朋友去參觀,驗票員如果知道他們是個集體,就可以把他們看成一個整體(鎖租化),一次性驗票過,而不需要一個個找他們驗票。

總結

我們直接以一張Synchronized洋蔥圖作爲總結吧,如果你願意一層一層剝開我的心。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章