【Java併發編程】2四、Synchronized實現原理解析

1、概述

咱們知道在JDK1.5以前synchronized是一個重量級鎖,相對於j.u.c.Lock,它會顯得那麼笨重,以致於咱們認爲它不是那麼的高效而慢慢摒棄它。安全

不過,隨着後續Java版本更新對synchronized進行的各類優化後,synchronized並不會顯得那麼重了。好比在jdk1.7中,concurrentHashMap中使用ReenTrantLock保證線程安全,而到了jdk1.8,又換成了使用synchronized來保證線程安全。說明synchronized的性能已經能夠和ReenTrantLock相差很少了多線程

2、實現原理

一、底層原理

synchronized在軟件層面依賴JVM實現,而j.u.c.Lock在硬件層面依賴特殊的CPU指令實現。
synchronized加鎖的代碼塊在編譯以後,會生成monitorenter和monitorexit兩個方法,對應加鎖和解鎖。
兩個指令的執行是JVM經過調用操做系統的互斥原語mutex來實現,被阻塞的線程會被掛起、等待從新調度,會致使「用戶態和內核態」兩個態之間來回切換,對性能有較大影響。

二、詳細說明

monitorenter:每一個對象都有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:
  1. 若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者;
  2. 若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1;
  3. 若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權;
monitorexit:執行monitorexit的線程必須是objectref所對應的monitor的全部者。 指令執行時,monitor的進入數減1,若是減1後進入數爲0,那線程退出monitor,再也不是這個monitor的全部者。其餘被這個monitor阻塞的線程能夠嘗試去獲取這個 monitor 的全部權。

三、兩個隊列

Monitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每一個等待鎖的線程都會被封裝成ObjectWaiter對象 ),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時:性能

  1. 首先會進入 _EntryList 集合,當線程獲取到對象的monitor後,進入 _Owner區域並把monitor中的owner變量設置爲當前線程,同時monitor中的計數器count加1
  2. 若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒
  3. 若當前線程執行完畢,也將釋放monitor(鎖)並復位count的值,以便其餘線程進入獲取monitor(鎖)

四、公平性

當一個線程釋放監視器時,在入口區和等待區的等待線程都會去競爭監視器,synchronized是非公平鎖

五、內存結構

Monitor對象存在於每一個Java對象的對象頭Mark Word中(存儲的指針的指向),Synchronized鎖即是經過這種方式獲取鎖的,也是爲何Java中任意對象能夠做爲鎖的緣由。

3、Java虛擬機對synchronize的優化

一、鎖的狀態

鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖。可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。測試

二、自旋鎖和自適應自旋鎖

線程的阻塞和喚醒須要cpu進行用戶態和內核態的切換,切換過程會消耗cpu資源。若是佔用鎖的時間很是短,切換鎖消耗的資源就得不嘗試。優化

因此在這種狀況下,引入了自旋鎖和自適應自旋鎖(循環必定的次數判斷鎖是否已經釋放)操作系統

三、偏向鎖

在大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓同一線程得到鎖的代價更低,引進了偏向鎖。線程

偏向鎖使用CAS加鎖,代替了比較笨重的線程阻塞方式。而且在成功獲取鎖以後標記偏向線程,避免了同一線程屢次加鎖頻繁進行CAS操做。指針

CAS的全稱爲Compare-And-Swap,是一條CPU的原子指令,其做用是讓CPU比較後原子地更新某個位置的值,通過調查發現,其實現方式是基於硬件平臺的彙編指令,就是說CAS是靠硬件實現的,JVM只是封裝了彙編調用對象

加鎖處理流程隊列

  1. 檢測Mark Word是否爲可偏向狀態,便是否爲偏向鎖1,鎖標識位爲01;
  2. 若爲可偏向狀態,則測試線程ID是否爲當前線程ID,若是是,則執行步驟(5),不然執行步驟(3);
  3. 若是測試線程ID不爲當前線程ID,則經過CAS操做競爭鎖,競爭成功,則將Mark Word的線程ID替換爲當前線程ID,不然執行線程(4);
  4. 經過CAS競爭鎖失敗,證實當前存在多線程競爭狀況,當到達全局安全點,得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,而後被阻塞在安全點的線程繼續往下執行同步代碼塊;
  5. 執行同步代碼塊;
解鎖處理過程:
  1. 暫停擁有偏向鎖的線程;
  2. 判斷鎖對象是否還處於被鎖定狀態,否,則恢復到無鎖狀態(01),以容許其他線程競爭。是,則掛起持有鎖的當前線程,並將指向當前線程的鎖記錄地址的指針放入對象頭Mark Word,升級爲輕量級鎖狀態(00),而後恢復持有鎖的當前線程,進入輕量級鎖的競爭模式;

四、輕量級鎖

引入輕量級鎖的主要目的是 在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗

「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的。可是,首先須要強調一點的是,輕量級鎖並非用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。

輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,必然就會致使輕量級鎖膨脹爲重量級鎖。

加鎖步驟以下:

  1. 在線程進入同步塊時,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word
  2. 拷貝對象頭中的Mark Word複製到鎖記錄(Lock Record)中;
  3. 拷貝成功後,虛擬機將使用CAS操做嘗試將對象Mark Word中的Lock Word更新爲指向當前線程Lock Record的指針,並將Lock record裏的owner指針指向object mark word。若是更新成功,則執行步驟(4),不然執行步驟(5);
  4. 若是這個更新動做成功了,那麼當前線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,即表示此對象處於輕量級鎖定狀態
  5. 若是這個更新操做失敗了,虛擬機首先會檢查對象Mark Word中的Lock Word是否指向當前線程的棧幀,若是是,就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,進入自旋執行(3),若自旋結束時仍未得到鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,當前線程以及後面等待鎖的線程也要進入阻塞狀態。

五、重量級鎖

Synchronized是經過對象內部的一個叫作 監視器鎖(Monitor)來實現的可是監視器鎖本質又是依賴於底層的操做系統的Mutex Lock來實現的。而操做系統實現線程之間的切換這就須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized效率低的緣由。所以,這種依賴於操做系統Mutex Lock所實現的鎖咱們稱之爲 「重量級鎖」
 
 
 
參考:
相關文章
相關標籤/搜索