【多線程高併發】synchronized鎖升級過程及其實現原理

問:爲什麼會有鎖升級的過程呢?
答:在java6以前synchronized鎖實現都是重量級鎖的形式,效率低下,爲了提升效率進行了優化,所以出現了鎖升級的過程。
問:我們通常說synchronized鎖是重量級鎖,那麼爲什麼叫他重量級鎖?
答:因爲synchronized執行效率太低。在java1.6以前每次調用synchronized加鎖時都需要進行系統調用,系統調用會涉及到用戶態和內核態的切換,系統調用會經過0x80中斷,經過內核調用後再返回用戶態。此過程比較複雜時間比較長所以通常叫synchronized爲重量級鎖。
誤區:其實鎖升級過程中涉及到的鎖偏向鎖,輕量級鎖都是synchronized鎖的具體實現所要經歷的過程,他們並不是單獨的鎖。只是給他們這幾種鎖的狀態起了一個名字而已。

CAS

在介紹synchronized鎖升級過程之前,我們需要先了解cas的原理,爲什麼呢?因爲cas貫穿了整個synchronized鎖升級的過程。

CAS : compare and swap 或者 compare and exchange 比較交換。
當我們需要對內存中的數據進行修改操作時,爲了避免多線程併發修改的情況,我們在對他進行修改操作前,先讀取他原來的值E,然後進行計算得出新的的值V,在修改前去比較當前內存中的值N是否和我之前讀到的E相同,如果相同,認爲其他線程沒有修改過內存中的值,如果不同,說明被其他線程修改了,這時,要繼續循環去獲取最新的值E,再進行計算和比較,直到我們預期的值和當前內存中的值相等時,再對數據執行修改操作。

CAS具體流程如下下圖:
在這裏插入圖片描述
他是爲了實現java中的原子操作而出現的。爲了保證在比較完成後賦值這兩個操作的原子性,jvm內部實現cas操作時通過LOCK CMPXCHG指令鎖cpu總線方式實現原子操作的。

對象頭

synchronized用的鎖是存在java對象頭裏的。

32位java對象頭結構如下表所示:
在這裏插入圖片描述

對於64位的java對象頭其餘信息基本不變,只是中間有關於對象hashcode值和之後加鎖信息的位數加大以外,其他基本不變。
64位虛擬機系統下java對象頭在不同鎖狀態下的狀態變化如下表所示:
在這裏插入圖片描述
如上圖所示:其中最後兩位代表是否加鎖的標誌位。鎖標誌位如果是01的話需要根據前一位的是否爲偏向鎖來判斷當前的鎖狀態,如果前一位爲0則代表無鎖狀態,如果爲1則代表有偏向鎖。
後兩位:00代表輕量級鎖,10代表重量級鎖,11代表GC垃圾回收的標記信息。

偏向鎖

偏向鎖產生的原因?
大多數情況下,鎖不緊不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。

獲取偏向鎖流程:
當一個線程訪問同步塊時,會先判斷鎖標誌位是否爲01,如果是01,則判斷是否爲偏向鎖,如果是,會先判斷當前鎖對象頭中是否存儲了當前的線程id,如果存儲了,則直接獲得鎖。如果對象頭中指向不是當前線程id,則通過CAS嘗試將自己的線程id存儲進當前鎖對象的對象頭中來獲取偏向鎖。當cas嘗試獲取偏向鎖成功後則繼續執行同步代碼塊,否則等待安全點的到來撤銷原來線程的偏向鎖,撤銷時需要暫停原持有偏向鎖的線程,判斷線程是否活動狀態,如果已經退出同步代碼塊則喚醒新的線程開始獲取偏向鎖,否則開始鎖競爭進行鎖升級過程,升級爲輕量級鎖。

偏向鎖獲取流程如下圖:
在這裏插入圖片描述
在高併發下可以關閉偏向鎖來提升性能,通過設置JVM參數 -XX:-UseBiasedLocking=false。

輕量級鎖

當出現鎖競爭時,會升級爲輕量級鎖。
在升級輕量級鎖之前,JVM會先在當前線程的棧幀中創建用於存儲鎖記錄的空間即將對象頭中用來標記鎖信息相關的內容封裝成一個java對象放入當前線程的棧幀中,這個對象稱爲LockRcord,然後線程嘗試通過CAS將對象頭中mark word替換爲指向鎖記錄(lockrecord)的指針。如果成功則當前線程獲取鎖,如果失敗則使用自旋來獲取鎖。自旋其實就是不斷的循環進行CAS操作直到能成功替換。所以輕量級鎖又叫自旋鎖。

下圖來源於網絡
棧上分配LockRecord如下圖: lockrecord中包含了對象的引用地址。
在這裏插入圖片描述
對象頭中markword替換鎖記錄指針成功之後如下圖:
在這裏插入圖片描述

替換成功之後將鎖標誌位改爲00 表示獲取輕量級鎖成功。
lockrecord的作用:在這裏實現了鎖重入,每當同一個線程多次獲取同一個鎖時,會在當前棧幀中放入一個lockrecord,但是重入是放入的lockrecord關於鎖信息的內容爲null,代表鎖重入。當輕量級解鎖時,每解鎖一次則從棧幀中彈出一個lockrecord,直到爲0.
輕量級鎖重入之後如下圖:
在這裏插入圖片描述

當通過CAS自旋獲取輕量級鎖達到一定次數時,JVM會發生鎖膨脹升級爲重量級鎖。
原因:不斷的自旋在高併發的下會消耗大量的cpu資源,所以jvm爲了節省cpu資源,進行了鎖升級。將等待獲取鎖的線程都放入一個等待隊列中來節省cpu資源。

重量級鎖

在重量級鎖中將LockRecord對象替換爲了monitor對象的實現。主要通過monitorenter和monitorexit兩個指令來實現。需要經過系統調用,在併發低的情況下效率會低。
通過openJDK可以查看ObjectMonitor對象的結構:http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/9758d9f36299/src/share/vm/runtime/objectMonitor.hpp

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;
  }

使用monitor加鎖如下圖:
在這裏插入圖片描述
重量級鎖在進行鎖重入的時候每獲取到鎖一次會對monitor對象中的計數器+1,等鎖退出時則會相應的-1,直到減到0爲止,鎖完全退出。

幾種鎖狀態優缺點對比

在這裏插入圖片描述

總結

綜上,我們發現偏向鎖,輕量級鎖(又稱自旋鎖或無鎖),重量級鎖都是synchronized鎖鎖實現中鎖經歷的幾種不同的狀態。
三種鎖狀態的場景總結:

  • 只有一個線程進入臨界區 -------偏向鎖
  • 多個線程交替進入臨界區--------輕量級鎖
  • 多個線程同時進入臨界區-------重量級鎖
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章