線程間的同步與通訊(3)——淺析synchronized的實現原理

前言

系列文章目錄 java

前面兩篇文章咱們介紹了synchronized同步代碼塊以及wait和notify機制,大體知道了這些關鍵字和方法是幹什麼的,以及怎麼用。git

可是,知其然,並不知其因此然。github

例如:segmentfault

  1. 什麼是監視器鎖?
  2. JAVA中任何對象均可以做爲鎖,那麼鎖信息是怎麼被記錄和存儲的?
  3. 監視器鎖是怎樣被獲取的?
  4. 監視器鎖是怎樣被釋放的?
  5. 什麼是wait set

本篇咱們未來解答這些問題。數據結構

spin-lock 和 suspend-lock

總的來講,鎖有兩種不一樣的實現方式,一種是自旋,一種是掛起。併發

(suspend-lock不知道怎麼翻譯,感受叫"掛起鎖"或"懸掛鎖"都太難聽了,後面就直接不翻譯了) 性能

自旋鎖是一種樂觀鎖,它樂觀地認爲鎖資源沒有被佔用,或者即便被佔用了,也很快就會被釋放, 因此當它發現鎖已經被佔用後,大多會在原地忙等待(通常是在死循環中等待,這也就是自旋的由來), 直到鎖被釋放,咱們在以前分析AQS的文章中提過,AQS處在阻塞隊列頭部的線程用的就是自旋的方式來等待鎖。lua

suspend-lock是一種悲觀鎖,它悲觀地認爲鎖競爭老是常常發生的,若是鎖被佔用了,基本短期內不會釋放,因此他會讓出CPU資源,直接掛起,等待條件知足後,別人將本身喚醒。 spa

自旋鎖的優勢是實現簡單,只須要很小的內存,在競爭很少的場景中性能很好。可是若是鎖競爭不少,那麼大量的時間會浪費在無心義的自旋等待上,形成CPU利用率下降。操作系統

suspend-lock的優勢是CPU利用率高,由於在發現鎖被佔用後,它會當即釋放本身剩下的CPU時間隙(time-slice)給其餘線程,以指望得到更高的CPU利用率。可是由於線程的掛起與喚醒須要經過操做系統調用來完成,這涉及到用戶空間和內核空間的轉換,線程上下文的切換,因此即便在競爭不多的場景中,這種鎖也會顯得很慢。可是若是鎖競爭很激烈,則這種鎖就能夠得到很好的性能。

因而可知,自旋鎖和suspend-lock各有優劣,他們分別適用於競爭很少和競爭激烈的場景中。

在實際的應用中,咱們能夠綜合這兩種方式的優勢,例如AQS中,排在阻塞隊列第一位的使用自旋等待,而排在後面的線程則掛起。

而咱們今天要講的synchronized,使用的是suspend-lock方式。

synchronized的實現

既然前面提到了synchronized用的是suspend-lock方式,在看synchronized的實現原理以前,咱們不妨來思考一下: 若是要咱們本身設計,該怎麼作?

前幾篇咱們提到過:

每一個java對象均可以用作一個實現同步的鎖, 這些鎖被稱爲內置鎖(Intrinsic Lock)或者監視器鎖(Monitor Lock).

要實現這個目標,則每一個java對象都應該與某種類型的鎖數據關聯。

這就意味着,咱們須要一個存儲鎖數據的地方,而且每個對象都應該有這麼個地方。

在java中,這個地方就是對象頭。

其實Java的對象頭和對象的關係很像Http請求的http headerhttp body的關係。

對象頭中存儲了該對象的metadata, 除了該對象的鎖信息,還包括指向該對象對應的類的指針,對象的hashcode, GC分代年齡等,在對象頭這個寸土寸金的地方,根據鎖狀態的不一樣,有些內存是你們公用的,在不一樣的鎖狀態下,存儲不一樣的信息,而對象頭中存儲鎖信息的那部分字段,咱們稱做Mark Word, 這個咱們就不展開了講了。咱們只須要知道:

鎖信息存儲在對象頭的 Mark Word

在synchronized鎖中,這個存儲在對象頭的Mark Word中的鎖信息是一個指針,它指向一個monitor對象(也稱爲管程或監視器鎖)的起始地址。這樣,咱們就經過對象頭,將每個對象與一個monitor關聯了起來,它們的關係以下圖所示:

java fat lock
(圖片來源: Evaluating and improving biased locking in the HotSpot virtual machine

圖片的最左邊是線程的調用棧,它引用了堆中的一個對象,該對象的對象頭部分記錄了該對象所使用的監視器鎖,該監視器鎖指向了一個monitor對象。

那麼這個monitor對象是什麼呢? 在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構以下: (源碼在這裏)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
 }

上面這些字段中,咱們只須要重點關注三個字段:

  • _owner : 當前擁有該 ObjectMonitor 的線程
  • _EntryList: 當前等待鎖的集合
  • _WaitSet: 調用了Object.wait()方法而進入等待狀態的線程的集合,即咱們上一篇一直提到的wait set

在java中,每個等待鎖的線程都會被封裝成ObjectWaiter對象,當多個線程同時訪問一段同步代碼時,首先會被扔進 _EntryList 集合中,若是其中的某個線程得到了monitor對象,他將成爲 _owner,若是在它成爲 _owner以後又調用了wait方法,則他將釋放得到的monitor對象,進入 _WaitSet集合中等待被喚醒。

monitor對象

(圖片來源: Inter-thread communication in Java)

另外,由於每個對象均可以做爲synchronized的鎖,因此每個對象都必須支持wait()notifynotifyAll方法,使得線程可以在一個monitor對象上wait, 直到它被notify。這也就解釋了這三個方法爲何定義在了Object類中——這樣,全部的類都將持有這三個方法。

總結:

  1. 每個java對象都和一個ObjectMonitor對象相關聯,關聯關係存儲在該java對象的對象頭裏的Mark Word中。
  2. 每個試圖進入同步代碼塊的線程都會被封裝成ObjectWaiter對象,它們或在ObjectMonitor對象的_EntryList中,或在 _WaitSet 中,等待成爲ObjectMonitor對象的owner. 成爲owner的對象即獲取了監視器鎖。

因此,說是每個java對象均可以做爲鎖,實際上是指將每個java對象所關聯的ObjectMonitor做爲鎖,更進一步是指,你們都想成爲 某一個java對象所關聯的ObjectMonitor對象的_owner,因此你能夠把這個_owner看作是鐵王座,全部等待在這個監視器鎖上的線程都想坐上這個鐵王座,誰擁有了它,誰就有進入由它鎖住的同步代碼塊的權利。

附加題

其實,瞭解到上面這個程度已經足夠用了,若是你想再深刻的瞭解,例如synchronized在字節碼層面的具體語義實現,這裏推薦幾篇博客:

另外,若是你想深刻了解偏向鎖,輕量級鎖,以及鎖膨脹的過程,強烈建議看下面這篇論文:

該篇論文的介紹很是詳細,關鍵是有不少圖示,對於Mark Word在不一樣鎖狀態的描述很清晰。

(完)

查看更多系列文章:系列文章目錄

相關文章
相關標籤/搜索