摘要:前言本文是一篇簡短的雜糅本文源自于作者最近的一個疑問為什么在舊版的中偏向鎖的移除一定要在全局安全點進行同時在上個星期作者參與的一個項目發生了一件怪事一個服務莫名其妙地不接受任何請求了一切請求都是而查看日志發現出故障的服務本身去請求另一個服務
前言
本文是一篇簡短的雜糅.
本文源自于作者最近的一個疑問:為什么在舊版的jdk中偏向鎖的移除一定要在全局安全點進行?同時在上個星期,作者參與的一個項目發生了一件怪事:一個服務莫名其妙地不接受任何請求了,一切請求都是timeout,而查看日志,發現出故障的服務本身去請求另一個服務,請求與響應在幾十毫秒完成,卻原地停頓四十余秒,最后報出超時異常.
作者在調研這兩個問題期間搜索了大量以"偏向鎖"和"安全點"為關鍵詞的介紹,盡管最終也沒能找出準確的答案,但是在調研過程中還是有所得,值得記錄一個短篇了.
偏向鎖的疑問首先是偏向鎖的移除:
我們知道,從java6開始,自帶的synchronized鎖進行了大量的優化,有一個膨脹的過程,從無鎖-偏向鎖-輕量鎖-重量鎖依次膨脹,第一次加鎖時,允許線程將該監視器偏向自己,直到發生其他線程爭搶(偏向鎖持有線程在退出同步塊時不移除偏向,此種情況可以重偏向),此時偏向鎖被移除,并膨脹為輕量鎖.
這個過程可以簡單理解為其他線程請求鎖,虛擬機要所有線程在最近的安全點阻塞,vm線程偽造一個displaced?mark?word到持有者線程的棧楨,更改監視器的標記位,然后讓所有線程繼續執行.此時持有鎖的線程會因此自視為輕量鎖,競爭者也將按照輕量鎖的規則去競爭.
作者查看了大量的貼子和資料,哪怕是在官方的文章中,甚至一些貼了官方原碼的注釋中,也只有大概這樣的描述:偏向鎖的移除需要在全局安全點執行,就是不解釋為什么.
也許就沒有為什么吧,單純是官方的實現問題,在前面的文章"54個JAVA官方文檔術語"和"JAVA9-12"中曾簡單提過,從JAVA10起出現了一個新的功能"線程局部握手",它能幫助我們做若干事情,其中一件就是由vm線程和java線程在多帶帶線程的安全點移除偏向鎖,而不需要等待全局安全點,同時在握手期間,會阻止進入全局安全點.經過這么久的資料查找,作者看來在java10之前必須全局安全點才能移除偏向鎖這件事本身就似乎沒有為什么,只是從前就這樣設計的.
偏向鎖的存在意義:
我們知道,偏向鎖的目標是減少昂貴的原子指令cas等的使用以及互斥量的開銷;輕量鎖的目標是減少互斥量的開銷.偏向鎖在不考慮重偏向這種情況下,似乎只有第一次加鎖才起作用,那么這個問題似乎有些多余,我們會對沒有競爭的代碼加上同步嗎?
答案是會的.大體有以下場景:
1.類加載其實是加鎖的,我們可以嘗試并發地進行類加載,盡管大多情況下這由main線程完成.
2.一些舊版本的庫,如使用Vector,使用HashTable,使用Collections.synchronize系列,在絕對不會出現線程逃逸的情況下使用StringBuffer拼接字符串,單線程使用了某些庫中加了同步的代碼等.
3.默認的情況下在jvm啟動的前幾秒偏向鎖是不可用的,可以使用-XX:BiasedLockingStartupDelay=0進行配置.
以上情況可參考問題:偏向鎖的設計.
偏向鎖的設計疑問,為什么只在對象頭中保存線程id?
可以參考:偏向鎖與輕量鎖的設計不同.
偏向鎖退出同步塊其實是無操作的,偏向鎖標記依舊存在,所以自然恢復,規避了昂貴的原子指令和屏障的開銷,但是輕量鎖就不同了,需要在設置標記時保存鎖記錄的指針,同時還要將原來的信息存放到棧楨.這樣在釋放時,可以使用cas恢復原值.
Unlocked:
[ orig_header | 001 ] | Stack frame | | | Locked: | | [ stack_ptr | 000 ] | | | |-------------| --------------------->| orig_header | |-------------| | | | | -------------
重偏向問題:
偏向鎖的設計初衷是同一個線程一次或若干次往復地對同一個或幾個監視器加鎖,顯然只有首次需要一個原子指令.而jvm足夠地聰明,它會發現當前是否為值得偏向的無競態同步.
偏向鎖可以重偏向的一點細節:
1.HotSpot虛擬機僅支持"粗放"的重偏向(bulk rebias),用以在承受單隊列重偏向過程的開銷同時保留優化的收益.
2.粗放的偏向鎖重偏向和移除這兩件事共享了同一個安全點操作名:RevokeBias.
3.如果滿足這幾個條件:偏向鎖撤消次數超過了BiasedLockingBulkRebiasThreshold并且小于BiasedLockingBulkRevokeThresholdand,且最后一次撤消偏向不晚于BiasedLockingDecayTime,且所有逃逸的變量都限定于jvm的屬性,則后續的偏向鎖粗放重偏向是可用的.
4.使用-XX:+PrintSafepointStatistics可打印安全點事件,與偏向鎖有關的可重點可關注EnableBiasedLocking,RevokeBias和BulkRevokeBias.選項-XX:+TraceBiasedLocking可以幫助生成一個詳細描述jvm做出的偏向鎖決策的日志.
參考:單個偏向鎖的重偏向.
安全點和JIT關于安全點和JIT本身此處不再綴述,此處簡單回憶若干前提.
JIT有client和server模式,其中server模式是高度優化的,甚至于可以用"過度優化"來形容,在"54個java官方文檔術語"這篇文章中甚至提過一個"不常見的陷阱",發生時會反優化并退回解釋執行.
JIT高度編譯優化的代碼和字節碼解釋執行不同,可能會進行一些安全點的消除,并且編譯代碼要在全局安全點進行一次"棧上替換"(OSR),然后才能生效.
參考:循環的線程奇怪地阻塞了其他線程?
老外寫的一個代碼例子,非常像我們項目碰到的停頓現象,我們的代碼也類似,確實有大量的同步操作(必然涉及偏向鎖和移除,同時也涉及到JIT的棧上替換和計數大循環):
//代碼 public class TestBlockingThread { private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class); public static final void main(String[] args) throws InterruptedException { Runnable task = () -> { int i = 0; while (true) { i++; if (i != 0) { boolean b = 1 % i == 0; } } }; new Thread(new LogTimer()).start(); Thread.sleep(2000); new Thread(task).start(); } public static class LogTimer implements Runnable { @Override public void run() { while (true) { long start = System.currentTimeMillis(); try { Thread.sleep(1000); } catch (InterruptedException e) { // do nothing } LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start); } } } } //打印日志 [Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1004 [Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1003 [Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=13331 [Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1006 [Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1003 [Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1004 [Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
顯然中間那一個13秒多的等待時間就像我們項目中的40秒暫停一樣突兀,這也一度讓作者認為找對了答案.
該代碼中,每行日志的打印預期應該是間隔一秒上下,可以看到除了13秒多的一次停頓以外,其他操作的差距都是3-6毫秒的級別.
為什么會發生這樣的情況?
注意前面提到的前提,JIT編譯的高度優化代碼需要在全局安全點進行棧上替換,也就是說,它需要要求所有線程到最近的一個安全點阻塞.
正常情況下,每一個JAVA線程會輪詢一個安全點標記(safepoint?flag)來詢問是否要進入安全點,當觀察到去安全點標記(go?to?safepoint?flag)時,會趕去最近的安全點.但是,大量地進行安全點標記的輪詢是耗費性能的,因此C1C2編譯器做了相應的優化,消除了過于頻繁的安全點輪詢,因此安全點輪詢主要有以下幾種情況:
1.使用解釋器執行時任意兩個字節碼之間.
2.C1C2編譯器生成的代碼的非計數循環的"回邊"(參考了深入理解java虛擬機的回邊計數器,方法調用計數器的翻譯).
3.在C1C2編譯器的方法的退出(OpenJDK虛擬機)和進入(Zing),但當方法已經被內聯時,編譯器將移除這個安全點的輪詢點.
注意示例代碼的task線程,它進行的是一個計數的循環,因為計數的循環會讓編譯器認為是一個"有限"的循環,因此每個回邊不會插入相應的安全點輪詢.
故此,JIT在試圖將編譯優化的代碼進行OSR時,其他線程已趕到安全點阻塞,但是task線程卻依舊未能及時到達安全點,直到JIT最終放棄了等待并判定為無限循環為止.
解決方案:
1.增加選項-XX:+UseCountedLoopSafepoints?,可以看到問題立即消失了,但要注意,它會造成全局性能的永久下降,并可能造成jvm崩盤.加上這個選項后,編譯器會在每輪循環回邊進行安全點輪詢,問題解決.
2.顯式禁用某方法的編譯:-XX:CompileCommand="exclude,binary/class/Name,methodName
3.手動增加安全點輪詢,如在循環的結束處增加Thread.yield()或直接將計數器i改為long型(此時再回去翻doug大神的源碼,一定要思考yield和long型計數器),這樣循環會被編譯器認為是非常大的一個(雖然還不是無限).
答主還對原作者的循環代碼做出了一些修改,并解決了問題,而解決的原因就是利用了前面提過的"不常見的陷阱".
for (int i = OSR_value; i != 0; i++) { if (1 % i == 0) { uncommon_trap(); } } uncommon_trap();
明顯的一個問題,語義無變化,循環依舊是無限的.只不過在i自增到偶數時,編譯器將會遇到"不常見的陷阱",原本做出的極端優化將不得不退化為解釋器執行,從而解決了安全點輪詢過稀少的問題.
小結許多技術多帶帶來看都很好,偏向鎖,JIT,安全點.多帶帶看來都很完美,JIT的時間開銷也相對較少,但是結合在OSR真的是一大暗坑.
且不管偏向鎖為什么從前一定要在全局安全點移除了,作者后續會繼續查資料,總之,從JAVA10開始不用了.關于偏向鎖和OSR,建議閱讀此博客.
作者看來,安全點的機制特別像java官方提供的同步器,如前面介紹過的CyclicBarrier,CountDownLatch,Semaphore,Phaser.一定要等待所有線程到達某個點,然后再進行一些操作,操作完畢后再釋放線程繼續執行.
關于安全點的三個術語:
安全點狀態:java線程可以按相應的輪詢機制輪詢是否進入此狀態,但一旦進入,就只能在安全點操作結束后才可離開了.
安全點輪詢:java線程詢問是否需要進入安全點狀態的機制.
安全點操作:出于各種原因,但一定要等所有線程到達安全點才可以執行的操作.
最后上一張非常有代表性的圖,出自安全點有關的一個博客.
簡單介紹這個圖表的含義,它描述了安全點操作的若干開銷.
1.到達安全點的時間(Time to Safe Point?簡寫TTSP):每個要進入安全點的線程都能在命中安全點輪詢的情況下進入,但到達一個安全點輪詢所需執行的指令數是未知的,從圖上可以看到J1線程命中了一個安全點輪詢并掛起.J2和J3發生了對cpu時間的競態,J3提前獲取了cpu資源并使得J2壓入了運行隊列,但J2此時并不在安全點.J3到達了安全點并掛起,釋放了cpu資源,J2于是繼續執行并最終進行了安全點輪詢.J4和J5因為執行JNI代碼而早已處于安全點,它們在此處不受影響.但J5在安全點期間嘗試半路從JNI代碼回來而被掛起.所以我們可以看到,不同的線程到達安全點的時間變化很大,早到達的線程會停頓較長時間.
b.安全點操作的開銷:這取決于操作的類型,獲取棧跡(GetStackTrace)將取決于棧的深度,如果采樣了所有的線程或過多的線程(如在JAVA9-12一文中介紹過的新工具JVMTI::GetAllStackTraces),則時間也嚴重取決于線程數量.如果時間充裕,jvm會借此機會執行一些其他安全點操作.
c.恢復被掛起的線程的開銷.
上述問題分析的一些幫助:
a.過長的TTSP導致的停頓時間:這包含頁錯誤,cpu過載,過長的計數循環等.
b.線程的關閉與啟動的開銷與線程總數有關,總數越高則開銷越大,如果要計算總開銷,可以粗略使用非0的掛起/恢復線程開銷和TTSP乘以線程數量進行估算.
c.虛擬機參數-XX:+PrintGCApplicationStoppedTime可以列出所有的停頓時間和TTSP.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75509.html
摘要:近期在閱讀最新幾版的官方文檔過程中發現不少術語不清之處特發此文總結以下的術語大量在官方文檔中直接出現且直接如基本詞語一樣使用不理解它們會嚴重影響閱讀自適應自旋鎖自適應自旋鎖是一個允許線程在特定點自旋等待特定事件發生而不是直接進行并等待該事件 近期在閱讀JAVA最新幾版的官方文檔過程中發現不少術語不清之處,特發此文總結.以下的術語大量在官方文檔中直接出現,且直接如基本詞語一樣使用,不理解...
摘要:拆解虛擬機的基本步聚如下首先,要等待到自身成為唯一一個正在運行的非守護線程時,在整個等待過程中,虛擬機仍舊是可工作的。將相應的事件發送給,禁用,并終止信號線程。 本文簡單介紹HotSpot虛擬機運行時子系統,內容來自不同的版本,因此可能會與最新版本之間(當前為JDK12)存在一些誤差。 1.命令行參數處理HotSpot虛擬機中有大量的可影響性能的命令行屬性,可根據他們的消費者進行簡...
摘要:本篇博客主要針對虛擬機的晚期編譯優化,內存模型與線程,線程安全與鎖優化進行總結,其余部分總結請點擊虛擬總結上篇,虛擬機總結中篇。 本篇博客主要針對Java虛擬機的晚期編譯優化,Java內存模型與線程,線程安全與鎖優化進行總結,其余部分總結請點擊Java虛擬總結上篇 ,Java虛擬機總結中篇。 一.晚期運行期優化 即時編譯器JIT 即時編譯器JIT的作用就是熱點代碼轉換為平臺相關的機器碼...
摘要:典型地,和被用在等待另一個線程產生的結果的情形測試發現結果還沒有產生后,讓線程阻塞,另一個線程產生了結果后,調用使其恢復。使當前線程放棄當前已經分得的時間,但不使當前線程阻塞,即線程仍處于可執行狀態,隨時可能再次分得時間。 1、說說進程,線程,協程之間的區別 簡而言之,進程是程序運行和資源分配的基本單位,一個程序至少有一個進程,一個進程至少有一個線程.進程在執行過程中擁有獨立的內存單元...
閱讀 2104·2023-05-11 16:55
閱讀 3504·2021-08-10 09:43
閱讀 2618·2019-08-30 15:44
閱讀 2440·2019-08-29 16:39
閱讀 583·2019-08-29 13:46
閱讀 2005·2019-08-29 13:29
閱讀 921·2019-08-29 13:05
閱讀 691·2019-08-26 13:51