Java中的鎖的分類以及鎖優化
只要涉及到并發問題,同步往往并不可少的,而同步的實作比較簡單的方式就是加鎖,
Java鎖的種類
樂觀鎖/悲觀鎖
樂觀鎖:通俗地說就是不管是否有并發問題的風險,先進行操作,如果沒有其他執行緒爭用共享資料,那操作就成功了;如果共享的資料被爭用,產生了沖突,那再進行其他的補償措施,最常用的補償措施是不斷地重試,直到出現沒有競爭地共享資料為止;常見的樂觀鎖實作機制有兩種:
-
CAS操作
- CAS(Compare and Swap 比較并交換),當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試,
- CAS操作中包含三個運算元——需要讀寫的記憶體位置(V)、進行比較的預期原值(A)和擬寫入的新值(B),如果記憶體位置V的值與預期原值A相匹配,那么處理器會自動將該位置值更新為新值B,否則處理器不做任何操作,
- CAS是由一條原子性的處理器指令來完成的,保證了操作和沖突檢測這兩個步驟具有原子性,
-
資料版本機制
- 實作資料版本一般有兩種,第一種是使用版本號,第二種是使用時間戳,以版本號方式為例,
- 版本號方式:一般是在資料表中加上一個資料版本號version欄位,表示資料被修改的次數,當資料被修改時,version值會加一,當執行緒A要更新資料值時,在讀取資料的同時也會讀取version值,在提交更新時,若剛才讀取到的version值為當前資料庫中的version值相等時才更新,否則重試更新操作,直到更新成功,
悲觀鎖:也就是互斥同步,其總是認為只要不去做正確的同步措施(例如加鎖),那就肯定會出現問題,無論共享的資料是否真的會出現競爭,它都會進行加鎖,常見的悲觀鎖有synchronized、Lock等,
互斥鎖/讀寫鎖、獨享鎖/共享鎖
互斥鎖:顧名思義就是我得到了鎖后其他人就不能再進入得到這把鎖,除非我釋放了鎖,常見的互斥鎖就是synchronzed、ReentrantLock;獨享鎖與互斥鎖是同一種概念,
讀寫鎖:對于一個變數的操作與寫操作具有不同的同步機制;ReadWriteLock就是一個讀寫鎖,讀鎖的共享鎖可保證并發讀是非常高效的,讀寫,寫讀,寫寫的程序是互斥的;讀鎖就是一種共享鎖,
可重入鎖
可重入鎖:同一執行緒反復進入鎖住同一個變數的同步快不會出現將自己鎖死的情況,這就是可重復鎖;synchronized、ReentrantLock就是標準的可重入鎖,
公平鎖/非公平鎖
公平鎖:多個執行緒在等待同一個鎖時,必須按照申請鎖的時間順序(也就是先來后到)來依次獲得鎖;可以通過帶有true布林值的ReetrantLock構造器來創建公平鎖;在使用公平鎖時,會使得ReetrantLock的性能急劇下降,會明顯影響吞吐量,
非公平鎖:非公平鎖與公平鎖則相反;在鎖被釋放時,任何一個等待鎖的執行緒都有機會獲得鎖;典型的非公平鎖有synchronized和默認情況下的ReetrantLock,
自旋鎖/輕量級鎖/偏向鎖/重量級鎖
這四個名詞在下文中synchronized優化中進行講解,
分段鎖
分段鎖是一種鎖的設計理念,而不是具體的一種鎖,如果對一整個集合進行加鎖會使得效率降低;如果分段加鎖,在對某一段進行寫操作時,其他段的資料讀操作不會受到鎖影響可以提高訪問的效率,
在JDK8之前,Java中的ConcurrentHashMap使用的就是分段鎖機制,JDK8及以后使用的是CAS+synchronized,
鎖優化
HotSpot虛擬機開發團隊在JDk1.6這個版本上花費了大量的資源去實作各種鎖優化技術,包括鎖消除、鎖膨脹、自旋鎖、自適應自旋、輕量級鎖和偏向鎖等,
在講解所優化之前,我們需要先了解一下HotSpot虛擬機中物件的記憶體布局:
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ToGP0Sx3-1608176759515)(imgs/image-20200709151033237.png)]](https://img.uj5u.com/2020/12/18/206234181149461.png)
物件的記憶體布局不是本文的主講內容,這里就不詳細展開,我們主要看一下物件頭的運行時元資料這部分,
物件頭中運行時元資料的具體細節:
運行時元資料只要是哈希值、GC分代年齡、鎖狀態表示、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,這部分資料的長度在32位和64位的虛擬機(未開啟壓縮指標)中分別為32個bit和64個bit,官方稱它為"Mark Word",
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jGTTFtQZ-1608176759518)(imgs/markword_state.jpg)]](https://img.uj5u.com/2020/12/18/206234181149462.jpg)
自旋鎖
在很多情況下,共享資料的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復執行緒(涉及到從用戶態轉入到內核態)并不值得,所以只需要讓等待鎖的執行緒不阻塞,只是"等一下",但是不放棄處理器的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖,這里說的"等一下"其實就是讓執行緒去執行一個啥也不干的忙回圈,這就是自旋鎖,“通過忙回圈來鎖住自己”,
但是自旋的次數應該是有限度的,如果持有鎖的執行緒遲遲沒有釋放鎖,自旋的執行緒則是白白的浪費了很久的處理器資源(畢竟自旋也是需要處理器來執行操作的!);自旋的默認次數是十次,如果超過了限定的次數則使用普通的互斥同步來去掛起執行緒,
還好,JDK6的引入了自適應自旋,簡而言之就是經驗之談;
說白了就是根據上次通過自旋獲得該鎖的執行緒的自旋的次數來判斷這里是否可以進行自旋來等待鎖的釋放,如果上次別的執行緒去獲得這把鎖時自旋的次數很少,則說明當前想獲取鎖的執行緒也可以等待一下(別人能我也能!);如果上次別的執行緒去獲得這把鎖時自旋的次數很多,則虛擬機會認為沒有必要去自旋等待,直接使用普通的互斥同步來去掛起執行緒,
輕量級鎖
傳統的互斥同步鎖(synchronized)就是重量級鎖,在JDK6中引入了一種新的與重量級相對的“輕量級鎖”,它的設計初衷時在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的性能消耗,
輕量級鎖的作業程序:
- 在代碼即將進入同步塊的時候,如果此同步物件沒有被鎖定(Mark Word中鎖標志位"01"狀態),虛擬機首先將在當前執行緒的堆疊中建立一個名為鎖記錄(Lock Record)的空間,用于存盤物件目前的Mark Word的拷貝(官方稱為 Displaced Mark Word),
- 然后執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標,如果成功,當前執行緒獲得鎖,如果失敗,則自旋獲取鎖,當自旋獲取鎖仍然失敗時,表示存在其他執行緒競爭鎖(兩潭訓兩條以上的執行緒競爭同一個鎖),則輕量級鎖會膨脹成重量級鎖(鎖標志位"10"),
- 輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到物件頭,如果成功,則表示同步程序已完成,如果失敗,表示有其他執行緒嘗試過獲取該鎖,則要在釋放鎖的同時喚醒被掛起的執行緒,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7T2pRyDR-1608176759520)(imgs/light_lock_flow.jpg)]](https://img.uj5u.com/2020/12/18/206234181149463.png)
輕量級鎖說白了就是覺得大多數的鎖在使用程序中都是不存在競爭的,可以理解為一種“僥幸心理”:要是我用這把鎖的時候沒人來跟我搶,那不就不用加鎖了嘿嘿!
如果確實存在鎖競爭,因為輕量級鎖還需要進行CAS操作,反而比傳統的重量級鎖更慢,
偏向鎖
偏向鎖,顧名思義就是“偏”,偏心的“偏”,它的以實就是這個鎖會偏向于第一個獲得它的執行緒,如果在接下來的執行程序中,這把鎖一直沒有其他執行緒來過去,那么持有則這把鎖的執行緒則永遠不需要同步,偏向鎖的目的是消除資料在無競爭情況下的同步原語,
在沒有競爭的情況下,輕量級鎖最起碼還會進行CAS操作,而偏向鎖連CAS操作就直接忽略了!
偏向鎖的作業程序:
- 假設當前虛擬機啟用了偏向鎖,那么當鎖物件第一次被執行緒獲取的時候,虛擬機將會把物件頭中的標志位設定為“01”、把偏向模式設定為“1”,表示進入偏向模式,同時使用CAS操作把獲取到這個鎖的執行緒的ID記錄在物件的Mark Word之中,如果CAS操作成功,持有偏向鎖的執行緒以后每次進入這個鎖相關的同步鎖時,虛擬機都可以不再進行任何同步操作,
- 一旦出現另外一個執行緒去嘗試獲取這個鎖的情況,偏向模式就馬上宣告結束,根據鎖物件目前是否處理被鎖定的狀態決定是否撤銷偏向,撤銷后標志位恢復到未鎖定(01)或輕量級鎖定(00)的狀態,后序的同步操作與輕量級鎖的流程類似,偏向鎖、輕量級鎖的狀態轉換如下圖:
- [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qYiTn3wQ-1608176759522)(imgs/biased_lock_convert_flow.jpg)]
- 偏向鎖的釋放不需要做任何事情,這也就意味著加過偏向鎖的Mark Word會一直保留偏向鎖的狀態,因此即便同一個執行緒持續不斷地加鎖解鎖,也是沒有開銷的,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-eW9kmZeD-1608176759523)(imgs/biased_lock_flow.jpg)]](https://img.uj5u.com/2020/12/18/206234181149464.png)
因為偏向鎖的維護需要成本太高了,在JDK15中默認禁用了偏向鎖,準備在未來廢棄掉這項優化,
這里給出一張自旋鎖、輕量級鎖、偏向鎖和重量級鎖之間的關系圖:
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-A74zspk6-1608176759525)(imgs/Java鎖的升級.jpg)]](https://img.uj5u.com/2020/12/18/206234181149465.jpg)
鎖的降級
一個普遍的結論:鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖,這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率,
我認為這樣的結論是沒有錯的,畢竟鎖的降級效率較低,如果頻繁升降級的話對JVM性能會造成影響,
但其實Java中的重量級鎖在特定條件下是可以降級的,在JEP draft: Concurrent Monitor Deflation中指出重量級鎖降級發生在垃圾回收的STW(Stop The World)階段,降級物件為僅僅能被VMThread訪問而沒有其他JavaThread訪問的物件,
鎖的消除
鎖消除是指虛擬機的JIT(即時編譯器)在運行時,對一些代碼要求同步,但是對被檢測到不可能存在共享資料競爭的鎖進行消除,
鎖消除主要依賴于JVM的逃逸分析技術,所謂的逃逸分析,指的是JVM決議到一些變數在使用程序中不會被其他方法或者其他執行緒使用到,僅僅在當前執行緒/當前堆疊幀中使用,對于這些變數的同步操作就沒有必要按照傳統的方式進行加鎖/解鎖,增加沒有必要的資源浪費,
一個很簡答的例子:
public synchronized void Test(){
int n = 10;
for(int i = 1;i < n;i++ ){
....
}
}
這段代碼中的n和I都是區域變數,不會被其他方法/執行緒使用到,就沒有必要加同步手段了,
鎖的粗化
如果虛擬機探測到有這樣一串零碎的操作都對同一個物件加鎖,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部,這就是鎖的粗化,
一個例子:
public void Test(){
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
}
我們都知道,StringBuffer的append是加了synchronized的同步方法:
@Override
public synchronized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}
呼叫了三次append方法,若每次都加鎖/釋放鎖,勢必會造成資源的浪費,所以這里會優化為一次加鎖/釋放鎖,
參考內容:
- 深入理解Java虛擬機(JVM高級特性與最佳實踐)-----周志明
- http://luojinping.com/2015/07/09/java鎖優化/
- https://www.jianshu.com/p/9932047a89be
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/236640.html
標籤:其他
上一篇:javax.imageio.IIOException: Can‘t create output stream!(驗證碼圖片不顯示)
