關於鎖升級
java1.6之前Syntronized 沒有鎖升級概念,只有重量鎖:即用戶態和內核態的上下文切換 會比較浪費時間。
java1.6之後,Syntronized關鍵字 開始有鎖升級的概念,即偏向鎖,輕量級鎖,重量級鎖。
注意CAS不是自旋鎖,(CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。)自旋鎖是利用CAS做多次循環判斷。
所謂的鎖升級,其實就是從偏向鎖à輕量級鎖(自旋鎖)à重量級鎖,之前一直被這幾個概念困擾,網上的 文章解釋的又不通俗易懂,其實說白了,一切一切的開始源於java對synchronized同步機制的性能優化,最原始的synchronized同步機制是直接跳過前幾個步驟,直接進入重量級鎖的,而重量級鎖因爲需要線程進入阻塞狀態(從用戶態進入內核態)這種操作系統層面的操作非常消耗資源,這樣的話,synchronized同步機制就顯得很笨重,效率不高。那麼爲了解決這個問題,java才引入了偏向鎖,輕量級鎖,自旋鎖這幾個概念。拿這幾個鎖有何優化呢?網上也沒有通俗易懂的解釋,其實說白了就是,偏向鎖是爲了避免CAS操作,儘量在對比對象頭就把加鎖問題解決掉,只有衝突的情況下才指向一次CAS操作,而輕量級鎖和自旋鎖呢,其實兩個是一體使用的,爲的是儘量避免線程進入內核的阻塞狀態,這對性能非常不利,試圖用CAS操作和循環把加鎖問題解決掉,而重量級鎖是最終的無奈解決方案,說白了就是能通過內存讀取判斷解決加速問題優於〉通過CAS操作和空循環優於〉CPU阻塞,喚醒線程。
鎖種類
偏向鎖
因爲經過HotSpot的作者大量的研究發現,大多數時候是不存在鎖競爭的,常常是一個線程多次獲得同一個鎖,因此如果每次都要競爭鎖會增大很多沒有必要付出的代價,爲了降低獲取鎖的代價,才引入的偏向鎖。
輕量級鎖
輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因爲阻塞線程需要CPU從用戶態轉到內核態,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就乾脆不阻塞這個線程,讓它自旋這等待鎖釋放。
自旋鎖
減少線程上下文切換,提高執行效率
重量級鎖
除擁有鎖的線程外阻塞所有競爭線程
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狀態的線程,會被加入到該列表
}
光看這些數據結構對監視器鎖的工作機制還是一頭霧水,那麼我們首先看一下線程在獲取鎖的幾個狀態的轉換:
線程的生命週期存在5個狀態,start、running、waiting、blocking和dead
對於一個synchronized修飾的方法(代碼塊)來說:
當多個線程同時訪問該方法,那麼這些線程會先被放進_EntryList隊列,此時線程處於blocking狀態
當一個線程獲取到了實例對象的監視器(monitor)鎖,那麼就可以進入running狀態,執行方法,此時,ObjectMonitor對象的_owner指向當前線程,_count加1表示當前對象鎖被一個線程獲取
當running狀態的線程調用wait()方法,那麼當前線程釋放monitor對象,進入waiting狀態,ObjectMonitor對象的_owner變爲null,_count減1,同時線程進入_WaitSet隊列,直到有線程調用notify()方法喚醒該線程,則該線程重新獲取monitor對象進入_Owner區
如果當前線程執行完畢,那麼也釋放monitor對象,進入waiting狀態,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: 0
line 13: 10
}
可以看出方法開始和結束的地方都沒有出現monitorentry和monitorexit指令,但是出現的ACC_SYNCHRONIZED標誌位。
鎖的優化/升級過程
鎖升級
鎖的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編譯時(可以簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,經過逃逸分析,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間
參考:https://blog.csdn.net/tongdanping/article/details/79647337