|
synchronize基礎篇
Java共享模型帶來的線程安全問題
問題分析
兩個線程對初始值為 0 的靜態(tài)變量一個做自增,一個做自減,各做 5000 次,結果是 0 嗎? public class SyncDemo {
private static int counter = 0;
public static void increment() {
counter++;
}
public static void decrement() {
counter--;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
increment();
}
}, &#34;t1&#34;);
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
decrement();
}
}, &#34;t2&#34;);
t1.start();
t2.start();
t1.join();
t2.join();
//思考: counter=?
log.info(&#34;{}&#34;, counter);
}
- 以上的結果可能是正數(shù)、負數(shù)、零——因為 Java 中對靜態(tài)變量的自增,自減并不是原子操作
- 可以查看 i++和 i--(i 為靜態(tài)變量)的 JVM 字節(jié)碼指令 ( 可以在idea中安裝一個jclasslib插件)
getstatic i // 獲取靜態(tài)變量i的值
iconst_1 // 將int常量1壓入操作數(shù)棧
iadd // 自增
getstatic i // 獲取靜態(tài)變量i的值
iconst_1 // 將int常量1壓入操作數(shù)棧
isub // 自減
- 由上可看出
- 若是單線程以上代碼是順序執(zhí)行(不會交錯),結果沒有問題
- 若是多線程下代碼可能交錯運行,結果則不確定
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-1.jpg (22.58 KB, 下載次數(shù): 157)
下載附件
2023-4-9 14:40 上傳
臨界區(qū)( Critical Section)
- 一段代碼塊內(nèi)如果存在對共享資源的多線程讀寫操作,稱這段代碼塊為臨界區(qū),其共享資源為臨界資源
- 一個程序運行多個線程本身是沒有問題的
- 問題出在多個線程訪問共享資源
- 多個線程讀共享資源其實也沒有問題
- 在多個線程對共享資源讀寫操作時發(fā)生指令交錯,就會出現(xiàn)問題
//臨界資源
private static int counter = 0;
public static void increment() { //臨界區(qū)
counter++;
}
public static void decrement() {//臨界區(qū)
counter--;競態(tài)條件( Race Condition )
- 多個線程在臨界區(qū)內(nèi)執(zhí)行,由于代碼的執(zhí)行序列不同而導致結果無法預測,稱之為發(fā)生了競態(tài)條件
- 避免臨界區(qū)的競態(tài)條件發(fā)生的手段:
- 阻塞式的解決方案:synchronized,Lock
- 非阻塞式的解決方案:原子變量
【注意】
雖然 java 中互斥和同步都可以采用 synchronized 關鍵字來完成,但它們還是有區(qū)別的:
- 互斥是保證臨界區(qū)的競態(tài)條件發(fā)生,同一時刻只能有一個線程執(zhí)行臨界區(qū)代碼
- 同步是由于線程執(zhí)行的先后、順序不同、需要一個線程等待其它線程運行到某個點
synchronize的使用
synchronize加鎖方式
- synchronized 同步塊是 Java 提供的一種原子性內(nèi)置鎖
- Java 中的每個對象都可以把 synchronize 當作一個同步鎖來使用,這些 Java 內(nèi)置的,而使用者看不到的鎖,稱為內(nèi)置鎖,也叫監(jiān)視器鎖
加鎖方式分類
- 方法
- 代碼塊
- 實例對象
- class對象
- 任意實例對象Object
分類 | 具體分類 | 被鎖的對象 | 偽代碼 | 方法 | 實例方法 | 類的實例對象 | public synchronize void method(){} | 靜態(tài)方法 | 類對象 | public static synchronize void method(){} | 代碼塊 | 實例對象 | 類的實例對象 | synchronize(this){} | class對象 | 類對象 | synchronize(SynchronizeDemo.class){} | 任意實例對象object | 實例對象object | // String 對象作鎖String lock = &#34;&#34;;synchronize(lock){} | 解決共享問題方法
public static synchronized void increment() {
counter++;
}
public static synchronized void decrement() {
counter--;
}
private static String lock = &#34;&#34;;
public static void increment() {
synchronized (lock){
counter++;
}
}
public static void decrement() {
synchronized (lock) {
counter--;
}
}
- synchronized 實際是用對象鎖保證了臨界區(qū)內(nèi)代碼的原子性
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-2.jpg (39.44 KB, 下載次數(shù): 163)
下載附件
2023-4-9 14:40 上傳
synchronize高級篇——底層原理
synchronize底層原理
JVM指令層面synchronize實現(xiàn)原理
- synchronized是JVM內(nèi)置鎖,基于Monitor機制實現(xiàn)
- 其依賴底層操作系統(tǒng)的互斥原語Mutex(互斥量)
- 它是一個重量級鎖,性能較低
- JVM內(nèi)置鎖在1.5之后版本做了重大的優(yōu)化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、自適應自旋(Adaptive Spinning)等技術來減少鎖操作的開銷,內(nèi)置鎖的并發(fā)性能已經(jīng)基本與Lock持平
- Java虛擬機通過一個同步結構支持方法和方法中的指令序列的同步:monitor。
- 同步方法,是通過方法中的access_flags中設置ACC_SYNCHRONIZED標志來實現(xiàn);
- 同步代碼塊,是通過monitorenter和monitorexit來實現(xiàn)
- 兩個指令的執(zhí)行是JVM通過調(diào)用操作系統(tǒng)的互斥原語mutex來實現(xiàn),被阻塞的線程會被掛起、等待重新調(diào)度,會導致“用戶態(tài)和內(nèi)核態(tài)”兩個態(tài)之間來回切換,對性能有較大影響
管程(Monitnor)之MESA模型詳解
管程
- 管程是指管理共享變量以及對共享變量操作的過程,讓它們支持并發(fā)
- Java 1.5之前,Java語言提供的唯一并發(fā)語言就是管程
- synchronized關鍵字和wait()、notify()、notifyAll()這三個方法是Java中實現(xiàn)管程技術的組成部分
管程實現(xiàn)模型
- Hasen模型
- Hoare模型
- MESA模型——現(xiàn)在廣泛使用的
MESA模型
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-3.jpg (55.5 KB, 下載次數(shù): 168)
下載附件
2023-4-9 14:40 上傳
- 管程中引入了條件變量的概念,而且每個條件變量都對應有一個等待隊列
- 條件變量和等待隊列的作用是解決線程之間的同步問題
wait()的正確使用姿勢
while(條件不滿足) {
wait();
}
- 喚醒的時間和獲取到鎖繼續(xù)執(zhí)行的時間是不一致的,被喚醒的線程再次執(zhí)行時可能條件又不滿足了,所以循環(huán)檢驗條件
- MESA模型的wait()方法還有一個超時參數(shù),為了避免線程進入等待隊列永久阻塞
notify()和notifyAll()分別何時使用
- 滿足以下三個條件時,可以使用notify(),其余情況盡量使用notifyAll():
- 所有等待線程擁有相同的等待條件;
- 所有等待線程被喚醒后,執(zhí)行相同的操作;
- 只需要喚醒一個線程
Java的內(nèi)置管程synchronize
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-4.jpg (28.23 KB, 下載次數(shù): 174)
下載附件
2023-4-9 14:40 上傳
- Java 參考了 MESA 模型,語言內(nèi)置的管程(synchronized)對 MESA 模型進行了精簡
- MESA 模型中,條件變量可以有多個,Java 語言內(nèi)置的管程里只有一個條件變量
Monitnor機制在Java中的實現(xiàn)
- java.lang.Object 類定義了 wait(),notify(),notifyAll() 方法,這些方法的具體實現(xiàn),依賴于 ObjectMonitor 實現(xiàn)——這是 JVM 內(nèi)部基于 C++ 實現(xiàn)的一套機制
- ObjectMonitor其主要數(shù)據(jù)結構如下(hotspot源碼ObjectMonitor.hpp)
ObjectMonitor() {
_header = NULL; //對象頭 markOop
_count = 0;
_waiters = 0,
_recursions = 0; // 鎖的重入次數(shù)
_object = NULL; //存儲鎖對象
_owner = NULL; // 標識擁有該monitor的線程(當前獲取鎖的線程)
_WaitSet = NULL; // 等待線程(調(diào)用wait)組成的雙向循環(huán)鏈表,_WaitSet是第一個節(jié)點
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多線程競爭鎖會先存到這個單向鏈表中 (FILO棧結構)
FreeNext = NULL ;
_EntryList = NULL ; //存放在進入或重新進入時被阻塞(blocked)的線程 (也是存競爭鎖失敗的線程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-5.jpg (47.87 KB, 下載次數(shù): 143)
下載附件
2023-4-9 14:40 上傳
在獲取鎖時,是將當前線程插入到cxq的頭部,而釋放鎖時,默認策略(QMode=0)是:如果EntryList為空,則將cxq中的元素按原有順序插入到EntryList,并喚醒第一個線程,也就是當EntryList為空時,是后來的線程先獲取鎖。_EntryList不為空,直接從_EntryList中喚醒線程 對象內(nèi)存布局&對象頭詳解
對象內(nèi)存布局
Hotspot虛擬機中,對象在內(nèi)存中存儲的布局可以分為三塊區(qū)域:
- 對象頭(Header):比如 hash碼,對象所屬的年代,對象鎖,鎖狀態(tài)標志,偏向鎖(線程)ID,偏向時間,數(shù)組長度(數(shù)組對象才有)等
- 實例數(shù)據(jù)(Instance Data):存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息
- 對齊填充(Padding):虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍,所以為了字節(jié)對齊引入對齊填充,非必須
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-6.jpg (47.64 KB, 下載次數(shù): 159)
下載附件
2023-4-9 14:40 上傳
對象頭
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-7.jpg (61.19 KB, 下載次數(shù): 163)
下載附件
2023-4-9 14:40 上傳
HotSpot虛擬機的對象頭包括:
- Mark Word
- 用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳等他,這部分數(shù)據(jù)官方稱它為“Mark Word”
- 這部分數(shù)據(jù)的長度
- 在32位和64位的虛擬機中分別為32bit和64bit
- Klass Pointer
- klass類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例
- 32位4字節(jié),64位開啟指針壓縮或最大堆內(nèi)存<32g時4字節(jié),否則8字節(jié)
- jdk1.8默認開啟指針壓縮后為4字節(jié),當在JVM參數(shù)中關閉指針壓縮(-XX:-UseCompressedOops)后,長度為8字節(jié)
- 數(shù)組長度(只有數(shù)組對象有)
- 如果對象是一個數(shù)組, 那在對象頭中還必須有一塊數(shù)據(jù)用于記錄數(shù)組長度
- 長度
Mark word是如何記錄鎖狀態(tài)的
- 簡單點:MarkWord 結構搞得復雜,是因為需要節(jié)省內(nèi)存,讓同一個內(nèi)存區(qū)域在不同階段有不同的用處
Mark Word的結構
- hash: 保存對象的哈希碼
- 運行期間調(diào)用System.identityHashCode()來計算,延遲計算,并把結果賦值到這里
- age: 保存對象的分代年齡
- 表示對象被GC的次數(shù),當該次數(shù)到達閾值的時候,對象就會轉(zhuǎn)移到老年代
- biased_lock: 偏向鎖標識位
- 由于無鎖和偏向鎖的鎖標識都是 01,沒辦法區(qū)分,這里引入一位的偏向鎖標識位
- lock: 鎖狀態(tài)標識位
- 區(qū)分鎖狀態(tài),比如11時表示對象待GC回收狀態(tài), 只有最后2位鎖標識(11)有效
- JavaThread*: 保存持有偏向鎖的線程ID(線程的地址)
- 偏向模式的時候,當某個線程持有對象的時候,對象這里就會被置為該線程的ID。 在后面的操作中,就無需再進行嘗試獲取鎖的動作。
- 這個線程ID并不是JVM分配的線程ID號,和Java Thread中的ID是兩個概念。
- epoch: 保存偏向時間戳。偏向鎖在CAS鎖操作過程中,偏向性標識,表示對象更偏向哪個鎖
- ptr_to_lock_record:輕量級鎖狀態(tài)下,指向棧中鎖記錄的指針
- 當鎖獲取是無競爭時,JVM使用原子操作而不是OS互斥,這種技術稱為輕量級鎖定
- 在輕量級鎖定的情況下,JVM通過CAS操作在對象的Mark Word中設置指向鎖記錄的指針
- ptr_to_heavyweight_monitor:重量級鎖狀態(tài)下,指向?qū)ο蟊O(jiān)視器Monitor的指針
- 如果兩個不同的線程同時在同一個對象上競爭,則必須將輕量級鎖定升級到Monitor以管理等待的線程
- 在重量級鎖定的情況下,JVM在對象的ptr_to_heavyweight_monitor設置指向Monitor的指針
32位JVM下的對象結構描述
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-8.jpg (41.28 KB, 下載次數(shù): 155)
下載附件
2023-4-9 14:40 上傳
64位JVM下的對象結構描述
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-9.jpg (46.28 KB, 下載次數(shù): 173)
下載附件
2023-4-9 14:40 上傳
Mark Word中鎖標記枚舉
enum { locked_value = 0, //00 輕量級鎖
unlocked_value = 1, //001 無鎖
monitor_value = 2, //10 監(jiān)視器鎖,也叫膨脹鎖,也叫重量級鎖
marked_value = 3, //11 GC標記
biased_lock_pattern = 5 //101 偏向鎖
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-10.jpg (46.26 KB, 下載次數(shù): 157)
下載附件
2023-4-9 14:40 上傳
利用JOL工具跟蹤鎖標記變化
偏向鎖
- 偏向鎖是一種針對加鎖操作的優(yōu)化手段
- 在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為了消除數(shù)據(jù)在無競爭情況下鎖重入(CAS操作)的開銷而引入偏向鎖
- 對于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果
/***StringBuffer內(nèi)部同步***/
public synchronized int length() {
return count;
}
//System.out.println 無意識的使用鎖
public void println(String x) {
synchronized (this) {
print(x); newLine();
}
}
- 當JVM啟用了偏向鎖模式(jdk6默認開啟),新創(chuàng)建對象的Mark Word中的Thread Id為0,說明此時處于可偏向但未偏向任何線程,也叫做匿名偏向狀態(tài)(anonymously biased)
偏向鎖延遲偏向
- 偏向鎖模式存在偏向鎖延遲機制:HotSpot 虛擬機在啟動后有個 4s 的延遲才會對每個新建的對象開啟偏向鎖模式
- JVM啟動時會進行一系列的復雜活動(比如裝載配置,系統(tǒng)類初始化等等),在這個過程中會使用大量synchronized關鍵字對對象加鎖,且這些鎖大多數(shù)都不是偏向鎖。為了減少初始化時間,JVM默認延時加載偏向鎖
//關閉延遲開啟偏向鎖
-XX:BiasedLockingStartupDelay=0
//禁止偏向鎖
-XX:-UseBiasedLocking
//啟用偏向鎖
@Slf4j
public class LockEscalationDemo{
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
}
}偏向鎖狀態(tài)跟蹤
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虛擬機在啟動后有個 4s 的延遲才會對每個新建的對象開啟偏向鎖模式
Thread.sleep(4000);
Object obj = new Object();
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+&#34;開始執(zhí)行。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+&#34;獲取鎖執(zhí)行中。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+&#34;釋放鎖。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
}
},&#34;thread1&#34;).start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
}偏向鎖撤銷場景(升級或釋放)
調(diào)用對象HashCode
調(diào)用鎖對象的obj.hashCode()或System.identityHashCode(obj)方法會導致該對象的偏向鎖被撤銷
- 因為對于一個對象,其HashCode只會生成一次并保存,偏向鎖是沒有地方保存hashcode
- 輕量級鎖會在鎖記錄中記錄 hashCode
- 重量級鎖會在 Monitor 中記錄 hashCode
當對象處于可偏向(也就是線程ID為0)和已偏向的狀態(tài)下,調(diào)用HashCode計算將會使對象再也無法偏向:
- 當對象可偏向時,MarkWord將變成未鎖定狀態(tài),并只能升級成輕量鎖;
- 當對象正處于偏向鎖時,調(diào)用HashCode將使偏向鎖強制升級成重量鎖
調(diào)用wait/notify
- 偏向鎖狀態(tài)執(zhí)行obj.notify()會升級為輕量級鎖
- 調(diào)用obj.wait(timeout) 會升級為重量級鎖
輕量級鎖
- 倘若偏向鎖失敗,虛擬機并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優(yōu)化手段,此時Mark Word 的結構也變?yōu)檩p量級鎖的結構
- 輕量級鎖所適應的場景
- 線程交替執(zhí)行同步塊的場景
- 如果存在同一時間多個線程訪問同一把鎖的場合,就會導致輕量級鎖膨脹為重量級鎖
輕量級鎖跟蹤
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虛擬機在啟動后有個 4s 的延遲才會對每個新建的對象開啟偏向鎖模式
Thread.sleep(4000);
Object obj = new Object();
// 思考: 如果對象調(diào)用了hashCode,還會開啟偏向鎖模式嗎
obj.hashCode();
//log.debug(ClassLayout.parseInstance(obj).toPrintable());
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+&#34;開始執(zhí)行。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+&#34;獲取鎖執(zhí)行中。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+&#34;釋放鎖。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
}
},&#34;thread1&#34;).start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
}鎖升級場景:TEST
偏向鎖升級為輕量級鎖
輕量級鎖升級(膨脹)為重量級鎖
總結:鎖對象狀態(tài)轉(zhuǎn)換
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-11.jpg (87.05 KB, 下載次數(shù): 168)
下載附件
2023-4-9 14:40 上傳
synchronize(一)梳理
深入理解synchronize
- 應用場景:解決線程安全問題
- 加鎖,目的:序列化的訪問臨界資源
- Java中哪些實現(xiàn)鎖機制
- 阻塞式
- synchronize
- reentrantLock
- 非阻塞式
synchronize的使用
- 使用方式
- 原理
- JVM指令層面
- 方法上:acc_synchronize
- 代碼塊:monitorenter、monitorexit
- 管程(Monitor)
- Java 鎖體系的設計思想、設計理論基礎
- 實現(xiàn)模型
- MESA(最常用)
- 入口等待隊列(互斥)
- 多個條件對象(同步,阻塞喚醒機制)
- Java中實現(xiàn)monitor(簡化版的MESA)
- 基于Object實現(xiàn),方法:wait/notify/notifyAll,這些方法依賴ObjectMonitor實現(xiàn)
- 入口等待隊列:_EntryList、_cxq(棧結構);不公平機制
- 條件隊列:_waitSet
- 管程操作是重量級操作
- 涉及到用戶態(tài)到內(nèi)核態(tài)的切換
- 重量級鎖
- 競爭激烈的場景
- 膨脹期間創(chuàng)建一個monitor對象
- monitor對象可復用
- CAS 自旋,若失敗則阻塞
- 基本都到內(nèi)核態(tài)處理
- 因優(yōu)化重量級鎖,引入
- 偏向鎖
- 偏向某個線程,始終只有一個線程可進入到同步塊
- 如偏向thread1,其后續(xù)進入同步塊的邏輯,沒有加鎖解鎖的開銷
- 不存在競爭的場景,用偏向鎖
- 偏向鎖解鎖,還是偏向鎖狀態(tài)(結合鎖狀態(tài)輪轉(zhuǎn)圖看)
- 偏向鎖撤銷鎖且當前對象未鎖定,才變成無鎖狀態(tài);若當前對象鎖定,則變?yōu)檩p量級鎖
- 輕量級鎖
- 線程間存在輕微的競爭(即線程交替執(zhí)行,臨界區(qū)邏輯簡單,可很快執(zhí)行)
- 通過 CAS 獲取鎖,若失敗,則膨脹為重量級鎖——無自旋
- 會創(chuàng)建鎖記錄,通過CAS將markword內(nèi)容寫入鎖記錄,并將鎖狀態(tài)指向markword,雙向指向
- 加鎖/解鎖
- 加鎖解鎖的標記
- 識別是哪種鎖(鎖狀態(tài))
- ——由synchronize加鎖在對象上,是如何記錄鎖的?引出對象內(nèi)存布局
- 對象內(nèi)存布局
- 跟蹤鎖的狀態(tài)
- 理解誤區(qū)
- 關于偏向鎖、輕量級鎖、重量級鎖存在的誤區(qū)
- 1、無鎖-->偏向鎖-->輕量級鎖-->重量級鎖
- 不存在無鎖-->偏向鎖
- 且反過來鎖沒有降級,是轉(zhuǎn)到無鎖
- 2、輕量級鎖自旋獲取失敗,會膨脹為重量級鎖
- 輕量級鎖不存在自旋
- 3、重量級鎖不存在自旋
- 重量級鎖存在自旋
鎖升級的原理分析
輕量級鎖源碼分析
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-12.jpg (500.89 KB, 下載次數(shù): 156)
下載附件
2023-4-9 14:40 上傳
重量級鎖源碼分析——Synchronized重量級鎖加鎖解鎖執(zhí)行邏輯
synchronize中的三種鎖的形式在使用應用場景上有什么區(qū)別 ...-13.jpg (658.44 KB, 下載次數(shù): 149)
下載附件
2023-4-9 14:40 上傳
synchronize進階篇——synchronize鎖的優(yōu)化
針對偏向鎖的優(yōu)化
批量重偏向(bulk rebias)和批量撤銷(bulk revoke)機制
- 從偏向鎖的加鎖解鎖過程中可看出,當只有一個線程反復進入同步塊時,偏向鎖帶來的性能開銷基本可以忽略,但是當有其他線程嘗試獲得鎖時,就需要等到safe point時,再將偏向鎖撤銷為無鎖狀態(tài)或升級為輕量級,會消耗一定的性能
- 所以在多線程競爭頻繁的情況下,偏向鎖不僅不能提高性能,還會導致性能下降
- 于是,就有了批量重偏向(bulk rebias)與批量撤銷(bulk revoke)機制
原理
每個class對象會有一個對應的epoch字段,每個處于偏向鎖狀態(tài)對象的Mark Word中也有該字段,其初始值為創(chuàng)建該對象時class中的epoch的值。每次發(fā)生批量重偏向時,就將該值+1,同時遍歷JVM中所有線程的棧,找到該class所有正處于加鎖狀態(tài)的偏向鎖,將其epoch字段改為新值。下次獲得鎖時,發(fā)現(xiàn)當前對象的epoch值和class的epoch不相等,那就算當前已經(jīng)偏向了其他線程,也不會執(zhí)行撤銷操作,而是直接通過CAS操作將其Mark Word的Thread Id 改成當前線程Id。
- 批量重偏向原理
- 以class為單位,為每個class維護一個偏向鎖撤銷計數(shù)器(epoch字段),每一次該class的對象發(fā)生偏向撤銷操作時,該計數(shù)器+1,當這個值達到重偏向閾值(默認20)時,JVM就認為該class的偏向鎖有問題,因此會進行批量重偏向
- 即——當撤銷偏向鎖閾值超過 20 次后,jvm 會這樣覺得,我是不是偏向錯了,于是會在給這些對象加鎖時重新偏向至加鎖線程,重偏向會重置對象 的 Thread ID
- 批量撤銷原理
- 當達到重偏向閾值(默認20)后,假設該class計數(shù)器繼續(xù)增長,當其達到批量撤銷的閾值后(默認40),JVM就認為該class的使用場景存在多線程競爭,會標記該class為不可偏向,之后,對于該class的鎖,直接走輕量級鎖的邏輯
應用場景
- 批量重偏向機制場景
- 為了解決:一個線程創(chuàng)建了大量對象并執(zhí)行了初始的同步操作,后來另一個線程也來將這些對象作為鎖對象進行操作,這樣會導致大量的偏向鎖撤銷操作
- 批量撤銷場景
- 為了解決:在明顯多線程競爭劇烈的場景下使用偏向鎖是不合適的
JVM參數(shù)設置
- 設置JVM參數(shù)-XX:+PrintFlagsFinal,在項目啟動時即可輸出JVM的默認參數(shù)值
- 可以通過-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 來手動設置閾值
intx BiasedLockingBulkRebiasThreshold = 20 //默認偏向鎖批量重偏向閾值
intx BiasedLockingBulkRevokeThreshold = 40 //默認偏向鎖批量撤銷閾值測試:批量重偏向
- 當撤銷偏向鎖閾值超過 20 次后,jvm 會這樣覺得:
- 是不是偏向錯了,于是會在給這些對象加鎖時重新偏向至加鎖線程,重偏向會重置對象 的 Thread ID
@Slf4j
public class BiasedLockingTest {
//延時產(chǎn)生可偏向?qū)ο?br />
Thread.sleep(5000);
// 創(chuàng)建一個list,來存放鎖對象
List<Object> list = new ArrayList<>();
// 線程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建鎖對象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
//為了防止JVM線程復用,在創(chuàng)建完對象后,保持線程thead1狀態(tài)為存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, &#34;thead1&#34;).start();
//睡眠3s鐘保證線程thead1創(chuàng)建對象完成
Thread.sleep(3000);
log.debug(&#34;打印thead1,list中第20個對象的對象頭:&#34;);
log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 線程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if(i>=15&&i<=21||i>=38){
log.debug(&#34;thread2-第&#34; + (i + 1) + &#34;次加鎖執(zhí)行中\(zhòng)t&#34;+
ClassLayout.parseInstance(obj).toPrintable());
}
}
if(i==17||i==19){
log.debug(&#34;thread2-第&#34; + (i + 1) + &#34;次釋放鎖\t&#34;+
ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, &#34;thead2&#34;).start();
LockSupport.park();
}測試:批量撤銷
- 當撤銷偏向鎖閾值超過 40 次后,jvm 會認為不該偏向
- 于是整個類的所有對象都會變?yōu)椴豢善虻?,新建的對象也是不可偏向?br />
- 注意:時間-XX:BiasedLockingDecayTime=25000ms范圍內(nèi)沒有達到40次,撤銷次數(shù)清為0,重新計時
@Slf4j
public class BiasedLockingTest {
public static void main(String[] args) throws InterruptedException {
//延時產(chǎn)生可偏向?qū)ο?br />
Thread.sleep(5000);
// 創(chuàng)建一個list,來存放鎖對象
List<Object> list = new ArrayList<>();
// 線程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建鎖對象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
//為了防止JVM線程復用,在創(chuàng)建完對象后,保持線程thead1狀態(tài)為存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, &#34;thead1&#34;).start();
//睡眠3s鐘保證線程thead1創(chuàng)建對象完成
Thread.sleep(3000);
log.debug(&#34;打印thead1,list中第20個對象的對象頭:&#34;);
log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 線程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if(i>=15&&i<=21||i>=38){
log.debug(&#34;thread2-第&#34; + (i + 1) + &#34;次加鎖執(zhí)行中\(zhòng)t&#34;+
ClassLayout.parseInstance(obj).toPrintable());
}
}
if(i==17||i==19){
log.debug(&#34;thread2-第&#34; + (i + 1) + &#34;次釋放鎖\t&#34;+
ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, &#34;thead2&#34;).start();
Thread.sleep(3000);
new Thread(() -> {
for (int i = 0; i < 50; i++) {
Object lock =list.get(i);
if(i>=17&&i<=21||i>=35&&i<=41){
log.debug(&#34;thread3-第&#34; + (i + 1) + &#34;次準備加鎖\t&#34;+
ClassLayout.parseInstance(lock).toPrintable());
}
synchronized (lock){
if(i>=17&&i<=21||i>=35&&i<=41){
log.debug(&#34;thread3-第&#34; + (i + 1) + &#34;次加鎖執(zhí)行中\(zhòng)t&#34;+
ClassLayout.parseInstance(lock).toPrintable());
}
}
}
},&#34;thread3&#34;).start();
Thread.sleep(3000);
log.debug(&#34;查看新創(chuàng)建的對象&#34;);
log.debug((ClassLayout.parseInstance(new Object()).toPrintable()));
LockSupport.park();
}小結
- 批量重偏向和批量撤銷是針對類的優(yōu)化,和對象無關
- 偏向鎖重偏向一次之后不可再次重偏向
- 當某個類已經(jīng)觸發(fā)批量撤銷機制后,JVM會默認當前類產(chǎn)生了嚴重的問題,剝奪了該類的新實例對象使用偏向鎖的權利
針對重量級鎖的優(yōu)化
自旋優(yōu)化——(1.6之后有了自適應自旋)
- 重量級鎖競爭的時候,可使用自旋來進行優(yōu)化——如果當前線程自旋成功(即這時候持鎖線程已經(jīng)退出了同步塊,釋放了鎖),這時當前線程就可以避免阻塞
- 自旋會占用 CPU 時間,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發(fā)揮優(yōu)勢。
- 在 Java 6 之后自旋是自適應的,比如對象剛剛的一次自旋操作成功過,那么認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,比較智能
- Java 7 之后不能控制是否開啟自旋功能
- 注意
- 自旋的目的是為了減少線程掛起的次數(shù),盡量避免直接掛起線程(掛起操作涉及系統(tǒng)調(diào)用,存在用戶態(tài)和內(nèi)核態(tài)切換,這才是重量級鎖最大的開銷)
鎖粗化
- 假設一系列的連續(xù)操作都會對同一個對象反復加鎖及解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,即使沒有出現(xiàn)線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗
- 如果JVM檢測到有一連串零碎的操作都是對同一對象的加鎖,將會擴大加鎖同步的范圍(即鎖粗化)到整個操作序列的外部
StringBuffer buffer = new StringBuffer();
/**
* 鎖粗化
*/
public void append(){
buffer.append(&#34;aaa&#34;).append(&#34; bbb&#34;).append(&#34; ccc&#34;);
}鎖消除
- 鎖消除即刪除不必要的加鎖操作
- 鎖消除是Java虛擬機在JIT編譯期間,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖
- 通過鎖消除,可以節(jié)省毫無意義的請求鎖時間
public class LockEliminationTest {
/**
* 鎖消除
* -XX:+EliminateLocks 開啟鎖消除(jdk8默認開啟)
* -XX:-EliminateLocks 關閉鎖消除
* @param str1
* @param str2
*/
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
public static void main(String[] args) throws InterruptedException {
LockEliminationTest demo = new LockEliminationTest();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
demo.append(&#34;aaa&#34;, &#34;bbb&#34;);
}
long end = System.currentTimeMillis();
System.out.println(&#34;執(zhí)行時間:&#34; + (end - start) + &#34; ms&#34;);
}
}
- 示例中,StringBuffer的append是個同步方法,但是append方法中的 StringBuffer 屬于一個局部變量,不可能從該方法中逃逸出去,因此其實這過程是線程安全的,可以將鎖消除
逃逸分析
- 逃逸分析——JDK6 之后加入該技術
- 一種可有效減少Java 程序中同步負載和內(nèi)存堆分配壓力的跨函數(shù)全局數(shù)據(jù)流分析算法
- 通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上
- 逃逸分析的基本行為就是分析對象動態(tài)作用域
方法逃逸(對象逃出當前方法)
- 當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他地方中
線程逃逸((對象逃出當前線程)
- 這個對象甚至可能被其它線程訪問到,例如賦值給類變量或可以在其它線程中訪問的實例變量
逃逸分析使用/作用
使用逃逸分析,編譯器可以對代碼做如下優(yōu)化
- 同步省略或鎖消除(Synchronization Elimination)。如果一個對象被發(fā)現(xiàn)只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。
- 將堆分配轉(zhuǎn)化為棧分配(Stack Allocation)。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。
- 分離對象或標量替換(Scalar Replacement)。有的對象可能不需要作為一個連續(xù)的內(nèi)存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內(nèi)存,而是存儲在CPU寄存器中。
JVM參數(shù)指定是否開啟逃逸分析
-XX:+DoEscapeAnalysis //表示開啟逃逸分析 (jdk1.8默認開啟)
-XX:-DoEscapeAnalysis //表示關閉逃逸分析。
-XX:+EliminateAllocations //開啟標量替換(默認打開) |
|