天天看點

并發程式設計--synchronized的實作原理(二)

上一篇部落格​​Thread--synchronized示例(一)​​中我們簡單介紹了一下有關synchronized相關的知識,接下來我們詳細介紹一下synchronized的實作原理。

在多線程并發程式設計中synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖,随着對其的實作的各種優化,在有些情況下他就并不那麼重了,為了減少擷取鎖和釋放鎖帶來的性能消耗而引入了偏向鎖和輕量級鎖,以及對鎖的存儲結構進行更新。

Java中每一個對象都可以作為鎖,具體表現為以下3中形式:

(1)對于普通同步方法,鎖是目前執行個體對象

(2)對于靜态同步方法,鎖是目前來的Class對象

(3)對于同步方法塊,鎖是synchronized括号裡配置的對象。

當一個線程試圖通路同步代碼塊時,它首先必須得到鎖,退出和抛出異常時必須釋放鎖。那麼鎖到底存在哪裡呢?鎖裡面會存儲什麼資訊呢?

從JVM規範中可以看到Synchonized在JVM裡的實作原理,JVM基于進入和退出Monitor對象來實作方法同步和代碼塊同步,但兩者的實作細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實作的,而方法同步是使用另外一種方式實作的,細節在JVM規範裡并沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實作。

monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它将處于鎖定狀态。線程執行到monitorenter指令時,将會嘗試擷取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。

一、Java對象頭

synchronized用的鎖是存在Java對象頭裡的。如果對象是數組類型,則虛拟機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。

Java對象頭裡的Mark Word裡預設存儲對象的HashCode、分代年齡和鎖标記位。32位JVM的Mark Word的預設存儲結構如表2-3所示。

并發程式設計--synchronized的實作原理(二)

在運作期間,Mark Word裡存儲的資料會随着鎖标志位的變化而變化。Mark Word可能變化為存儲以下4種資料,如表2-4所示。

并發程式設計--synchronized的實作原理(二)

Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀态,級别從低到高依次是:無鎖狀态、偏向鎖狀态、輕量級鎖狀态和重量級鎖狀态,這幾個狀态會随着競争情況逐漸更新。鎖可以更新但不能降級,意味着偏向鎖更新成輕量級鎖後不能降級成偏向鎖。這種鎖更新卻不能降級的政策,目的是為了提高獲得鎖和釋放鎖的效率

JDK為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖,鎖一共有4種狀态,級别從低到高依次是:無鎖狀态,偏向鎖狀态、輕量級鎖狀态和重量級鎖狀态,這幾個狀态會随着競争情況逐漸更新。鎖可以更新但不能降級,目的是為了提高獲得鎖和釋放鎖 的效率。

二、同步原理

JVM規範規定JVM基于進入和退出Monitor對象來實作方法同步和代碼塊同步,但兩者的實作細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實作,而方法同步是使用另外一種方式實作的,細節在JVM規範裡并沒有詳細說明,但是方法的同步同樣可以使用這兩個指令來實作。monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處, JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個 monitor 與之關聯,當且一個monitor 被持有後,它将處于鎖定狀态。線程執行到 monitorenter 指令時,将會嘗試擷取對象所對應的 monitor 的所有權,即嘗試獲得對象的鎖。

三、偏向鎖

Hotspot的作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程通路同步塊并擷取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而隻需簡單的測試一下對象頭的Mark Word裡是否存儲着指向目前線程的偏向鎖,如果測試成功,表示線程已經獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的辨別是否設定成1(表示目前是偏向鎖),如果沒有設定,則使用CAS競争鎖,如果設定了,則嘗試使用CAS将對象頭的偏向鎖指向目前線程。

并發程式設計--synchronized的實作原理(二)

偏向鎖的撤銷:偏向鎖使用了一種等到競争出現才釋放鎖的機制,是以當其他線程嘗試競争偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活着,如果線程不處于活動狀态,則将對象頭設定成無鎖狀态,如果線程仍然活着,擁有偏向鎖的棧會被執行,周遊偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼重新偏向于其他線程,要麼恢複到無鎖或者标記對象不适合作為偏向鎖,最後喚醒暫停的線程。下圖中的線程1示範了偏向鎖初始化的流程,線程2示範了偏向鎖撤銷的流程。

​​

并發程式設計--synchronized的實作原理(二)

​​

關閉偏向鎖:偏向鎖在Java 6和Java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才激活,如有必要可以使用JVM參數來關閉延遲-XX:BiasedLockingStartupDelay = 0。如果你确定自己應用程式裡所有的鎖通常情況下處于競争狀态,可以通過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那麼預設會進入輕量級鎖狀态。

四、輕量級鎖

輕量級鎖加鎖:線程在執行同步塊之前,JVM會先在目前線程的棧桢中建立用于存儲鎖記錄的空間,并将對象頭中的Mark Word複制到鎖記錄中,官方稱為Displaced Mark Word。然後線程嘗試使用CAS将對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,目前線程獲得鎖,如果失敗,表示其他線程競争鎖,目前線程便嘗試使用自旋來擷取鎖。

并發程式設計--synchronized的實作原理(二)

輕量級鎖解鎖:輕量級解鎖時,會使用原子的CAS操作來将Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競争發生。如果失敗,表示目前鎖存在競争,鎖就會膨脹成重量級鎖。下圖是兩個線程同時争奪鎖,導緻鎖膨脹的流程圖。

​​

并發程式設計--synchronized的實作原理(二)

​​

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖更新成重量級鎖,就不會再恢複到輕量級鎖狀态。當鎖處于這個狀态下,其他線程試圖擷取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之争。

五、鎖的優缺點對比

優點 缺點 适用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 如果線程間存在鎖競争,會帶來額外的鎖撤銷的消耗。 适用于隻有一個線程通路同步塊場景。
輕量級鎖 競争的線程不會阻塞,提高了程式的響應速度。 如果始終得不到鎖競争的線程使用自旋會消耗CPU。

追求響應時間。

同步塊執行速度非常快。

重量級鎖 線程競争不使用自旋,不會消耗CPU。 線程阻塞,響應時間緩慢。

追求吞吐量。

同步塊執行速度較長。

參考文獻: