目錄
一、Synchronized使用場景
二、Synchronized實作原理
三、鎖的優化
1、鎖更新
2、鎖粗化
3、鎖消除
一、Synchronized使用場景
Synchronized是一個同步關鍵字,在某些多線程場景下,如果不進行同步會導緻資料不安全,而Synchronized關鍵字就是用于代碼同步。什麼情況下會資料不安全呢,要滿足兩個條件:一是資料共享(臨界資源),二是多線程同時通路并改變該資料。
例如:
- public class AccountingSync implements Runnable{
- //共享資源(臨界資源)
- static int i= 0;
- /**
- * synchronized 修飾執行個體方法
- */
- public synchronized void increase(){
- i++;
- }
- @Override
- public void run() {
- for( int j= 0;j< 1000000;j++){
- increase();
- }
- }
- public static void main(String[] args) throws InterruptedException {
- AccountingSync instance= new AccountingSync();
- Thread t1= new Thread(instance);
- Thread t2= new Thread(instance);
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(i);
- }
- }
該段程式的輸出為: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鎖對象是存在哪裡的呢?答案是存在鎖對象的對象頭的MarkWord中。那麼MarkWord在對象頭中到底長什麼樣,也就是它到底存儲了什麼呢?
在32位的虛拟機中:
在64位的虛拟機中:
上圖中的偏向鎖和輕量級鎖都是在java6以後對鎖機制進行優化時引進的,下文的鎖更新部分會具體講解,Synchronized關鍵字對應的是重量級鎖,接下來對重量級鎖在Hotspot JVM中的實作鎖講解。
2、Synchronized在JVM中的實作原理
重量級鎖對應的鎖标志位是10,存儲了指向重量級螢幕鎖的指針,在Hotspot中,對象的螢幕(monitor)鎖對象由ObjectMonitor對象實作(C++),其跟同步相關的資料結構如下:
- ObjectMonitor() {
- _count = 0; //用來記錄該對象被線程擷取鎖的次數
- _waiters = 0;
- _recursions = 0; //鎖的重入次數
- _owner = NULL; //指向持有ObjectMonitor對象的線程
- _WaitSet = NULL; //處于wait狀态的線程,會被加入到_WaitSet
- _WaitSetLock = 0 ;
- _EntryList = NULL ; //處于等待鎖block狀态的線程,會被加入到該清單
- }
光看這些資料結構對螢幕鎖的工作機制還是一頭霧水,那麼我們首先看一下線程在擷取鎖的幾個狀态的轉換:
線程的生命周期存在6個狀态,可以參考:一文搞懂線程世界級難題——線程狀态到底是6種還是5種!!!
對于一個synchronized修飾的方法(代碼塊)來說:
- 當多個線程同時通路該方法,那麼這些線程會先被放進_EntryList隊列,此時線程處于blocking狀态
- 當一個線程擷取到了執行個體對象的螢幕(monitor)鎖,那麼就可以進入RUNNABLED狀态,執行方法,此時,ObjectMonitor對象的_owner指向目前線程,_count加1表示目前對象鎖被一個線程擷取
- 當RUNNABLED狀态的線程調用wait()方法,那麼目前線程釋放monitor對象,進入waiting狀态,ObjectMonitor對象的_owner變為null,_count減1,同時線程進入_WaitSet隊列,直到有線程調用notify()方法喚醒該線程,喚醒的線程進入到_EntryList阻塞隊列裡,等到鎖被釋放後,線程才去搶奪鎖,搶到鎖的線程重新擷取monitor對象進入_Owner區
- 如果目前線程執行完畢,那麼也釋放monitor對象,進入TERMINATED狀态,ObjectMonitor對象的_owner變為null,_count減1
那麼Synchronized修飾的代碼塊/方法如何擷取monitor對象的呢?
在JVM規範裡可以看到,不管是方法同步還是代碼塊同步都是基于進入和退出monitor對象來實作,然而二者在具體實作上又存在很大的差別。通過javap對class位元組碼檔案反編譯可以得到反編譯後的代碼。
(1)Synchronized修飾代碼塊:
Synchronized代碼塊同步在需要同步的代碼塊開始的位置插入monitorentry指令,在同步結束的位置或者異常出現的位置插入monitorexit指令;JVM要保證monitorentry和monitorexit都是成對出現的,任何對象都有一個monitor與之對應,當這個對象的monitor被持有以後,它将處于鎖定狀态。
例如,同步代碼塊如下:
- public class SyncCodeBlock {
- public int i;
- public void syncTask(){
- synchronized ( this){
- i++;
- }
- }
- }
對同步代碼塊編譯後的class位元組碼檔案反編譯,結果如下(僅保留方法部分的反編譯内容):
- public void syncTask();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack= 3, locals= 3, args_size= 1
- 0: aload_0
- 1: dup
- 2: astore_1
- 3: monitorenter //注意此處,進入同步方法
- 4: aload_0
- 5: dup
- 6: getfield # 2 // Field i:I
- 9: iconst_1
- 10: iadd
- 11: putfield # 2 // Field i:I
- 14: aload_1
- 15: monitorexit //注意此處,退出同步方法
- 16: goto 24
- 19: astore_2
- 20: aload_1
- 21: monitorexit //注意此處,退出同步方法
- 22: aload_2
- 23: athrow
- 24: return
- Exception table:
- //省略其他位元組碼.......
可以看出同步方法塊在進入代碼塊時插入了monitorentry語句,在退出代碼塊時插入了monitorexit語句,為了保證不論是正常執行完畢(第15行)還是異常跳出代碼塊(第21行)都能執行monitorexit語句,是以會出現兩句monitorexit語句。
(2)Synchronized修飾方法:
Synchronized方法同步不再是通過插入monitorentry和monitorexit指令實作,而是由方法調用指令來讀取運作時常量池中的ACC_SYNCHRONIZED标志隐式實作的,如果方法表結構(method_info Structure)中的ACC_SYNCHRONIZED标志被設定,那麼線程在執行方法前會先去擷取對象的monitor對象,如果擷取成功則執行方法代碼,執行完畢後釋放monitor對象,如果monitor對象已經被其它線程擷取,那麼目前線程被阻塞。
同步方法代碼如下:
- public class SyncMethod {
- public int i;
- public synchronized void syncTask(){
- i++;
- }
- }
對同步方法編譯後的class位元組碼反編譯,結果如下(僅保留方法部分的反編譯内容):
- public synchronized void syncTask();
- descriptor: ()V
- //方法辨別ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
- flags: ACC_PUBLIC, ACC_SYNCHRONIZED
- Code:
- stack= 3, locals= 1, args_size= 1
- 0: aload_0
- 1: dup
- 2: getfield # 2 // Field i:I
- 5: iconst_1
- 6: iadd
- 7: putfield # 2 // Field i:I
- 10: return
- LineNumberTable:
- line 12:
- line 13: 10
- }
可以看出方法開始和結束的地方都沒有出現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)這幾種鎖的優缺點(偏向鎖、輕量級鎖、重量級鎖)
2、鎖粗化
按理來說,同步塊的作用範圍應該盡可能小,僅在共享資料的實際作用域中才進行同步,這樣做的目的是為了使需要同步的操作數量盡可能縮小,縮短阻塞時間,如果存在鎖競争,那麼等待鎖的線程也能盡快拿到鎖。
但是加鎖解鎖也需要消耗資源,如果存在一系列的連續加鎖解鎖操作,可能會導緻不必要的性能損耗。
鎖粗化就是将多個連續的加鎖、解鎖操作連接配接在一起,擴充成一個範圍更大的鎖,避免頻繁的加鎖解鎖操作。
3、鎖消除
Java虛拟機在JIT編譯時(可以簡單了解為當某段代碼即将第一次被執行時進行編譯,又稱即時編譯),通過對運作上下文的掃描,經過逃逸分析,去除不可能存在共享資源競争的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間
目錄