天天看點

Synchronized關鍵字和鎖更新(偏向鎖、輕量級鎖、重量級鎖)一、Synchronized使用場景二、Synchronized實作原理三、鎖的優化

目錄

一、Synchronized使用場景

二、Synchronized實作原理

三、鎖的優化

1、鎖更新

2、鎖粗化

3、鎖消除

一、Synchronized使用場景

Synchronized是一個同步關鍵字,在某些多線程場景下,如果不進行同步會導緻資料不安全,而Synchronized關鍵字就是用于代碼同步。什麼情況下會資料不安全呢,要滿足兩個條件:一是資料共享(臨界資源),二是多線程同時通路并改變該資料。

例如:

  1. public class AccountingSync implements Runnable{
  2. //共享資源(臨界資源)
  3. static int i= 0;
  4. /**
  5. * synchronized 修飾執行個體方法
  6. */
  7. public synchronized void increase(){
  8. i++;
  9. }
  10. @Override
  11. public void run() {
  12. for( int j= 0;j< 1000000;j++){
  13. increase();
  14. }
  15. }
  16. public static void main(String[] args) throws InterruptedException {
  17. AccountingSync instance= new AccountingSync();
  18. Thread t1= new Thread(instance);
  19. Thread t2= new Thread(instance);
  20. t1.start();
  21. t2.start();
  22. t1.join();
  23. t2.join();
  24. System.out.println(i);
  25. }
  26. }

該段程式的輸出為:2000000

但是如果increase的synchronized被删除,那麼很可能輸出結果就會小于2000000,這是因為多個線程同時通路臨界資源i,如果一個線程A對i=88的自增到89沒有被B線程讀取到,線程B認為i仍然是88,那麼線程B對i的自增結果還是89,那麼這裡就會出現問題。

Synchronized鎖的3種使用形式(使用場景):

  • Synchronized修飾普通同步方法:鎖對象目前執行個體對象;
  • Synchronized修飾靜态同步方法:鎖對象是目前的類Class對象;
  • Synchronized修飾同步代碼塊:鎖對象是Synchronized後面括号裡配置的對象,這個對象可以是某個對象(xlock),也可以是某個類(Xlock.class);

注意:

  • 使用synchronized修飾非靜态方法或者使用synchronized修飾代碼塊時制定的為執行個體對象時,同一個類的不同對象擁有自己的鎖,是以不會互相阻塞。
  • 使用synchronized修飾類和對象時,由于類對象和執行個體對象分别擁有自己的螢幕鎖,是以不會互相阻塞。
  • 使用使用synchronized修飾執行個體對象時,如果一個線程正在通路執行個體對象的一個synchronized方法時,其它線程不僅不能通路該synchronized方法,該對象的其它synchronized方法也不能通路,因為一個對象隻有一個螢幕鎖對象,但是其它線程可以通路該對象的非synchronized方法。
  • 線程A通路執行個體對象的非static synchronized方法時,線程B也可以同時通路執行個體對象的static synchronized方法,因為前者擷取的是執行個體對象的螢幕鎖,而後者擷取的是類對象的螢幕鎖,兩者不存在互斥關系。

二、Synchronized實作原理

1、Java對象頭

首先,我們要知道對象在記憶體中的布局:

已知對象是存放在堆記憶體中的,對象大緻可以分為三個部分,分别是對象頭、執行個體變量和填充位元組。

  • 對象頭的zhuyao是由MarkWord和Klass Point(類型指針)組成,其中Klass Point是是對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體,Mark Word用于存儲對象自身的運作時資料。如果對象是數組對象,那麼對象頭占用3個字寬(Word),如果對象是非數組對象,那麼對象頭占用2個字寬。(1word = 2 Byte = 16 bit)
  • 執行個體變量存儲的是對象的屬性資訊,包括父類的屬性資訊,按照4位元組對齊
  • 填充字元,因為虛拟機要求對象位元組必須是8位元組的整數倍,填充字元就是用于湊齊這個整數倍的
Synchronized關鍵字和鎖更新(偏向鎖、輕量級鎖、重量級鎖)一、Synchronized使用場景二、Synchronized實作原理三、鎖的優化

通過第一部分可以知道,Synchronized不論是修飾方法還是代碼塊,都是通過持有修飾對象的鎖來實作同步,那麼Synchronized鎖對象是存在哪裡的呢?答案是存在鎖對象的對象頭的MarkWord中。那麼MarkWord在對象頭中到底長什麼樣,也就是它到底存儲了什麼呢?

在32位的虛拟機中:

Synchronized關鍵字和鎖更新(偏向鎖、輕量級鎖、重量級鎖)一、Synchronized使用場景二、Synchronized實作原理三、鎖的優化

在64位的虛拟機中:

Synchronized關鍵字和鎖更新(偏向鎖、輕量級鎖、重量級鎖)一、Synchronized使用場景二、Synchronized實作原理三、鎖的優化

上圖中的偏向鎖和輕量級鎖都是在java6以後對鎖機制進行優化時引進的,下文的鎖更新部分會具體講解,Synchronized關鍵字對應的是重量級鎖,接下來對重量級鎖在Hotspot JVM中的實作鎖講解。

2、Synchronized在JVM中的實作原理

重量級鎖對應的鎖标志位是10,存儲了指向重量級螢幕鎖的指針,在Hotspot中,對象的螢幕(monitor)鎖對象由ObjectMonitor對象實作(C++),其跟同步相關的資料結構如下:

  1. ObjectMonitor() {
  2. _count = 0; //用來記錄該對象被線程擷取鎖的次數
  3. _waiters = 0;
  4. _recursions = 0; //鎖的重入次數
  5. _owner = NULL; //指向持有ObjectMonitor對象的線程
  6. _WaitSet = NULL; //處于wait狀态的線程,會被加入到_WaitSet
  7. _WaitSetLock = 0 ;
  8. _EntryList = NULL ; //處于等待鎖block狀态的線程,會被加入到該清單
  9. }

光看這些資料結構對螢幕鎖的工作機制還是一頭霧水,那麼我們首先看一下線程在擷取鎖的幾個狀态的轉換:

Synchronized關鍵字和鎖更新(偏向鎖、輕量級鎖、重量級鎖)一、Synchronized使用場景二、Synchronized實作原理三、鎖的優化

線程的生命周期存在6個狀态,可以參考:一文搞懂線程世界級難題——線程狀态到底是6種還是5種!!!

對于一個synchronized修飾的方法(代碼塊)來說:

  1. 當多個線程同時通路該方法,那麼這些線程會先被放進_EntryList隊列,此時線程處于blocking狀态
  2. 當一個線程擷取到了執行個體對象的螢幕(monitor)鎖,那麼就可以進入RUNNABLED狀态,執行方法,此時,ObjectMonitor對象的_owner指向目前線程,_count加1表示目前對象鎖被一個線程擷取
  3. 當RUNNABLED狀态的線程調用wait()方法,那麼目前線程釋放monitor對象,進入waiting狀态,ObjectMonitor對象的_owner變為null,_count減1,同時線程進入_WaitSet隊列,直到有線程調用notify()方法喚醒該線程,喚醒的線程進入到_EntryList阻塞隊列裡,等到鎖被釋放後,線程才去搶奪鎖,搶到鎖的線程重新擷取monitor對象進入_Owner區
  4. 如果目前線程執行完畢,那麼也釋放monitor對象,進入TERMINATED狀态,ObjectMonitor對象的_owner變為null,_count減1

那麼Synchronized修飾的代碼塊/方法如何擷取monitor對象的呢?

在JVM規範裡可以看到,不管是方法同步還是代碼塊同步都是基于進入和退出monitor對象來實作,然而二者在具體實作上又存在很大的差別。通過javap對class位元組碼檔案反編譯可以得到反編譯後的代碼。

(1)Synchronized修飾代碼塊:

Synchronized代碼塊同步在需要同步的代碼塊開始的位置插入monitorentry指令,在同步結束的位置或者異常出現的位置插入monitorexit指令;JVM要保證monitorentry和monitorexit都是成對出現的,任何對象都有一個monitor與之對應,當這個對象的monitor被持有以後,它将處于鎖定狀态。

例如,同步代碼塊如下:

  1. public class SyncCodeBlock {
  2. public int i;
  3. public void syncTask(){
  4. synchronized ( this){
  5. i++;
  6. }
  7. }
  8. }

對同步代碼塊編譯後的class位元組碼檔案反編譯,結果如下(僅保留方法部分的反編譯内容):

  1. public void syncTask();
  2. descriptor: ()V
  3. flags: ACC_PUBLIC
  4. Code:
  5. stack= 3, locals= 3, args_size= 1
  6. 0: aload_0
  7. 1: dup
  8. 2: astore_1
  9. 3: monitorenter //注意此處,進入同步方法
  10. 4: aload_0
  11. 5: dup
  12. 6: getfield # 2 // Field i:I
  13. 9: iconst_1
  14. 10: iadd
  15. 11: putfield # 2 // Field i:I
  16. 14: aload_1
  17. 15: monitorexit //注意此處,退出同步方法
  18. 16: goto 24
  19. 19: astore_2
  20. 20: aload_1
  21. 21: monitorexit //注意此處,退出同步方法
  22. 22: aload_2
  23. 23: athrow
  24. 24: return
  25. Exception table:
  26. //省略其他位元組碼.......

可以看出同步方法塊在進入代碼塊時插入了monitorentry語句,在退出代碼塊時插入了monitorexit語句,為了保證不論是正常執行完畢(第15行)還是異常跳出代碼塊(第21行)都能執行monitorexit語句,是以會出現兩句monitorexit語句。

(2)Synchronized修飾方法:

Synchronized方法同步不再是通過插入monitorentry和monitorexit指令實作,而是由方法調用指令來讀取運作時常量池中的ACC_SYNCHRONIZED标志隐式實作的,如果方法表結構(method_info Structure)中的ACC_SYNCHRONIZED标志被設定,那麼線程在執行方法前會先去擷取對象的monitor對象,如果擷取成功則執行方法代碼,執行完畢後釋放monitor對象,如果monitor對象已經被其它線程擷取,那麼目前線程被阻塞。

同步方法代碼如下:

  1. public class SyncMethod {
  2. public int i;
  3. public synchronized void syncTask(){
  4. i++;
  5. }
  6. }

對同步方法編譯後的class位元組碼反編譯,結果如下(僅保留方法部分的反編譯内容):

  1. public synchronized void syncTask();
  2. descriptor: ()V
  3. //方法辨別ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
  4. flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  5. Code:
  6. stack= 3, locals= 1, args_size= 1
  7. 0: aload_0
  8. 1: dup
  9. 2: getfield # 2 // Field i:I
  10. 5: iconst_1
  11. 6: iadd
  12. 7: putfield # 2 // Field i:I
  13. 10: return
  14. LineNumberTable:
  15. line 12:
  16. line 13: 10
  17. }

可以看出方法開始和結束的地方都沒有出現monitorentry和monitorexit指令,但是出現的ACC_SYNCHRONIZED标志位。

三、鎖的優化

1、鎖更新

鎖的4中狀态:無鎖狀态、偏向鎖狀态、輕量級鎖狀态、重量級鎖狀态(級别從低到高)

(1)偏向鎖:

為什麼要引入偏向鎖?

因為經過HotSpot的作者大量的研究發現,大多數時候是不存在鎖競争的,常常是一個線程多次獲得同一個鎖,是以如果每次都要競争鎖會增大很多沒有必要付出的代價,為了降低擷取鎖的代價,才引入的偏向鎖。

偏向鎖的更新

當線程1通路代碼塊并擷取鎖對象時,會在java對象頭和棧幀中記錄偏向的鎖的threadID,因為偏向鎖不會主動釋放鎖,是以以後線程1再次擷取鎖的時候,需要比較目前線程的threadID和Java對象頭中的threadID是否一緻,如果一緻(還是線程1擷取鎖對象),則無需使用CAS來加鎖、解鎖;如果不一緻(其他線程,如線程2要競争鎖對象,而偏向鎖不會主動釋放是以還是存儲的線程1的threadID),那麼需要檢視Java對象頭中記錄的線程1是否存活,如果沒有存活,那麼鎖對象被重置為無鎖狀态,其它線程(線程2)可以競争将其設定為偏向鎖;如果存活,那麼立刻查找該線程(線程1)的棧幀資訊,如果還是需要繼續持有這個鎖對象,那麼暫停目前線程1,撤銷偏向鎖,更新為輕量級鎖,如果線程1 不再使用該鎖對象,那麼将鎖對象狀态設為無鎖狀态,重新偏向新的線程。

偏向鎖的取消:

偏向鎖是預設開啟的,而且開始時間一般是比應用程式啟動慢幾秒,如果不想有這個延遲,那麼可以使用-XX:BiasedLockingStartUpDelay=0;

如果不想要偏向鎖,那麼可以通過-XX:-UseBiasedLocking = false來設定;

(2)輕量級鎖

為什麼要引入輕量級鎖?

輕量級鎖考慮的是競争鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因為阻塞線程需要CPU從使用者态轉到核心态,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,是以這個時候就幹脆不阻塞這個線程,讓它自旋這等待鎖釋放。

輕量級鎖什麼時候更新為重量級鎖?

線程1擷取輕量級鎖時會先把鎖對象的對象頭MarkWord複制一份到線程1的棧幀中建立的用于存儲鎖記錄的空間(稱為DisplacedMarkWord),然後使用CAS把對象頭中的内容替換為線程1存儲的鎖記錄(DisplacedMarkWord)的位址;

如果線上程1複制對象頭的同時(線上程1CAS之前),線程2也準備擷取鎖,複制了對象頭到線程2的鎖記錄空間中,但是線上程2CAS的時候,發現線程1已經把對象頭換了,線程2的CAS失敗,那麼線程2就嘗試使用自旋鎖來等待線程1釋放鎖。

但是如果自旋的時間太長也不行,因為自旋是要消耗CPU的,是以自旋的次數是有限制的,比如10次或者100次,如果自旋次數到了線程1還沒有釋放鎖,或者線程1還在執行,線程2還在自旋等待,這時又有一個線程3過來競争這個鎖對象,那麼這個時候輕量級鎖就會膨脹為重量級鎖。重量級鎖把除了擁有鎖的線程都阻塞,防止CPU空轉。

*注意:為了避免無用的自旋,輕量級鎖一旦膨脹為重量級鎖就不會再降級為輕量級鎖了;偏向鎖更新為輕量級鎖也不能再降級為偏向鎖。一句話就是鎖可以更新不可以降級,但是偏向鎖狀态可以被重置為無鎖狀态。

(3)這幾種鎖的優缺點(偏向鎖、輕量級鎖、重量級鎖)

Synchronized關鍵字和鎖更新(偏向鎖、輕量級鎖、重量級鎖)一、Synchronized使用場景二、Synchronized實作原理三、鎖的優化

2、鎖粗化

按理來說,同步塊的作用範圍應該盡可能小,僅在共享資料的實際作用域中才進行同步,這樣做的目的是為了使需要同步的操作數量盡可能縮小,縮短阻塞時間,如果存在鎖競争,那麼等待鎖的線程也能盡快拿到鎖。 

但是加鎖解鎖也需要消耗資源,如果存在一系列的連續加鎖解鎖操作,可能會導緻不必要的性能損耗。 

鎖粗化就是将多個連續的加鎖、解鎖操作連接配接在一起,擴充成一個範圍更大的鎖,避免頻繁的加鎖解鎖操作。

3、鎖消除

Java虛拟機在JIT編譯時(可以簡單了解為當某段代碼即将第一次被執行時進行編譯,又稱即時編譯),通過對運作上下文的掃描,經過逃逸分析,去除不可能存在共享資源競争的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間

目錄

繼續閱讀