Synchronized
Synchronized涉及很多知識面,我們先從一些相關知識講起,
CAS
CAS 全稱 Compare And Swap (又稱Compare And Exchange) / 自旋 / 自旋鎖 / 無鎖
因為經常配合回圈操作,直到修改完成為止,所以泛指一類操作
執行原理

細節事項
1、保證原子性
? CAS操作雖然可以做到不需要主動上鎖就可以解決原子操作問題,但是其實在執行對比A與V的值時,在底層仍然是加鎖了的,不然無法避免指令亂序執行而導致的資料不一致,只是這個操作對我們程式員是無感知的,
? 這里拓展一下,在CAS的底層實作中,(多核)CPU實際呼叫了指令 lock cmpxch,事實上也是通過上鎖來保證CAS操作的原子性,只不過這個鎖實際鎖定的是北橋信號而不是記憶體總線,效率較高些,
? 具體可以參考文章CAS你以為你真的懂?
2、ABA問題
? ABA問題可以理解為,待修改物件值原本是A被修改為B后又改為了A,那么此時再單純的對比物件值就沒辦法確定是否應該更新了,
? 解決辦法使用版本號來確定修改版本(版本號 AtomicStampedReference),如果是基礎型別簡單值則不需要版本號,
內核態與用戶態
這個是作業系統中的一個概念,作業系統為了避免應用程式直接使用內核中的服務,把內核空間與用戶空間進行了分割,這也代表著,應用程式(如JVM)向作業系統申請內核服務或資源時,需要花費更多的時間與資源,
對synchronized有什么影響呢?
在早期JDK版本中實作synchronized,是通過向作業系統申請重量級鎖的方式實作的,而申請重量級鎖就必須通過作業系統的內核的允許再進行系統呼叫(以中斷的方式),如此一來,導致早期synchronied的實作特別低效,
注:*中斷和例外可以使用系統呼叫來呼叫內核方法,
后來出現的偏向鎖和輕量級鎖(自旋鎖)在用戶態中處理,所以效率也高了很多,
Markword
講到Java的markword那就必須要講到Java物件的記憶體布局了,
當我們創建一個物件,物件資訊會被jvm寫入記憶體,物件在記憶體中的分布如下圖(以下是64位的jvm環境,默認開啟壓縮指標的情況)

8位元組對齊
我們稱markword+class pointer = 12Byte ,這12位元組可以稱作object header,它管理著一個物件的各種屬性狀態,object header后面的內容則是物件的成員變數,
JVM對記憶體的管理有8位元組對齊的要求,所以我們可以看到1、3圖中都因為原始內容不足8*倍而進行了補齊填充,
但是他們的情況有點不一樣,一個是在類成員后面補充,一個則是在class pointer和long型別成員間補充,所以對齊填充是有2種情況的,這個也是我偶然發現的,,具體補齊的實作我沒有去查看細節,這個不作為重點討論,
Markword的位數對應關系
剛剛談到了我們稱markword+class pointer = 12Byte ,這12位元組可以稱作object header,它管理著一個物件的各種屬性狀態,
現在具體來講講Markword中與鎖相關的位對應著什么資訊,

我們直接看最后三位,首先,最后兩位決定了鎖的型別
-
0 1 :兩種可能
-
0 0 1:無鎖,新物件
-
1 0 1:偏向鎖
-
-
0 0 :自旋鎖
-
1 0 :重量級鎖
-
1 1 :GC標記
鎖升級
synchronized優化的程序和markword息息相關
markword上面已經看過了每一位的表示意義,現在來理解synchronized鎖升級的程序,

-
匿名偏向鎖
-XX:BiasedLockingStartupDelay會設定偏向鎖的啟動延時(默認4s),當JVM啟動時會有很多執行緒競爭,所以默認情況啟動時不直接打開偏向鎖,再一段時間后設為偏向鎖,此時為匿名偏向鎖,執行緒ID為0,
-
偏向鎖
偏向鎖特別的指向了某個執行緒,看markword-bit圖中可以看到寫入了執行緒指標,(一般此時毫無競爭,哪個執行緒來使用這把鎖,鎖就指向哪個執行緒)
-
輕量級鎖
又稱自旋鎖,偏向鎖在輕度競爭時會撤銷偏向鎖,升級輕量級鎖,也有可能從普通物件直接升級為輕量級鎖,如果產生了競爭,競爭的執行緒們會生成一個Lock Record于自己的執行緒堆疊中,并用CAS操作將markword設定為指向自己執行緒的LockRecord的指標,設定成功者得到鎖,其余沒有獲取到鎖的執行緒繼續自旋,
-
重量級鎖
競爭加劇,有執行緒超過10次自旋,( 由-XX:PreBlockSpin控制自旋次數,默認為10), 或者自旋執行緒數超過CPU核數的一半,升級重量級鎖,向作業系統申請資源, CPU從3級到0級系統呼叫,執行緒掛起,進入等待佇列,等待作業系統的調度,然后再映射回用戶空間(JAVA1.6之后,加入自適應自旋, JVM自己控制鎖升級)
為什么有自旋鎖還需要重量級鎖?
自旋是消耗CPU資源的,如果鎖的時間長,或者自旋執行緒多,CPU資源會被大量消耗
重量級鎖(ObjectMonitor)有等待佇列(WaitSet),所有拿不到鎖的進入等待佇列,不需要消耗CPU資源
偏向鎖是否一定比自旋鎖效率高?
不一定,在明確知道會有多執行緒競爭的情況下,偏向鎖肯定會涉及鎖撤銷,這時候直接使用自旋鎖
JVM啟動程序,會有很多執行緒競爭(明確),所以默認情況啟動時不打開偏向鎖,過一段兒時間再打開
如果計算過物件的hashCode,則物件無法進入偏向狀態!
輕量級鎖重量級鎖的hashCode存在與什么地方?
答案:執行緒堆疊中,輕量級鎖的LockRecord中,或是代表重量級鎖的ObjectMonitor的成員中
鎖重入
可重入鎖,它的可重入性表現在同一個執行緒可以多次獲得鎖,
首先synchronized就是可重入鎖,也必須是可重入鎖,否則子類呼叫父類的super.m()的操作就無法實作,
而且重入的次數必須被記錄,因為解鎖需要對應的重入次數,
- 偏向鎖/輕量級鎖:記錄重入次數在執行緒堆疊中,每多一次重入LockRecord就會創建多一個,解鎖時則洗掉LockRecord
- 重量級鎖:與上面類似的操作,鎖重入資訊記錄到ObjectMonitor
synchronized vs Lock (CAS)
在高爭用 高耗時的環境下synchronized效率更高
在低爭用 低耗時的環境下CAS效率更高
synchronized到重量級之后是等待佇列(不消耗CPU)
CAS(等待期間消耗CPU)
鎖消除 lock eliminate
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
我們都知道 StringBuffer 是執行緒安全的,因為它的關鍵方法都是被 synchronized 修飾過的,但我們看上面這段代碼,我們會發現,sb 這個參考只會在 add 方法中使用,不可能被其它執行緒參考(因為是區域變數,堆疊私有),因此 sb 是不可能共享的資源,JVM 會自動消除 StringBuffer 物件內部的鎖,
鎖粗化 lock coarsening
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
JVM 會檢測到這樣一連串的操作都對同一個物件加鎖(while 回圈內 100 次執行 append,沒有鎖粗化的就要進行 100 次加鎖/解鎖),此時 JVM 就會將加鎖的范圍粗化到這一連串的操作的外部(比如 while 體外),使得這一連串操作只需要加一次鎖即可,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/166750.html
標籤:Java
