dw做網(wǎng)站怎么發(fā)布全網(wǎng)營銷的公司
- 👏作者簡介:大家好,我是愛吃芝士的土豆倪,24屆校招生Java選手,很高興認(rèn)識(shí)大家
- 📕系列專欄:Spring源碼、JUC源碼
- 🔥如果感覺博主的文章還不錯(cuò)的話,請(qǐng)👍三連支持👍一下博主哦
- 🍂博主正在努力完成2023計(jì)劃中:源碼溯源,一探究竟
- 📝聯(lián)系方式:nhs19990716,加我進(jìn)群,大家一起學(xué)習(xí),一起進(jìn)步,一起對(duì)抗互聯(lián)網(wǎng)寒冬👀
文章目錄
- Java 內(nèi)存模型
- 可見性
- 退不出的循環(huán)
- 解決方法
- 可見性 vs 原子性
- 模式之兩階段終止
- 同步模式之 Balking
- 定義
- 實(shí)現(xiàn)
- 有序性
- 指令級(jí)并行原理
- 名詞
- Clock Cycle Time
- CPI
- IPC
- CPU 執(zhí)行時(shí)間
- 魚罐頭的故事
- 指令重排序優(yōu)化
- 支持流水線的處理器
- SuperScalar 處理器
- 詭異的結(jié)果
- 解決方法
- volatile 原理
- 如何保證可見性
- 如何保證有序性
- double-checked locking 問題
- double-checked locking 解決
- 可見性
- 有序性
- happens-before
- 線程解鎖 m 之前對(duì)變量的寫,對(duì)于接下來對(duì) m 加鎖的其它線程對(duì)該變量的讀可見
- 線程對(duì) volatile 變量的寫,對(duì)接下來其它線程對(duì)該變量的讀可見
- 線程 start 前對(duì)變量的寫,對(duì)該線程開始后對(duì)該變量的讀可見
- 線程結(jié)束前對(duì)變量的寫,對(duì)其它線程得知它結(jié)束后的讀可見(比如其它線程調(diào)用 t1.isAlive() 或 t1.join()等待它結(jié)束)
- 線程 t1 打斷 t2(interrupt)前對(duì)變量的寫,對(duì)于其他線程得知 t2 被打斷后對(duì)變量的讀可見(通過t2.interrupted 或 t2.isInterrupted)
- 具有傳遞性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
- balking 模式習(xí)題
- 線程安全單例習(xí)題
- 實(shí)現(xiàn)1
- 實(shí)現(xiàn)2(※※※重點(diǎn)難點(diǎn)※※※)
- 實(shí)現(xiàn)3:
- 實(shí)現(xiàn)4:DCL
- 實(shí)現(xiàn)5
Java 內(nèi)存模型
JMM 即 Java Memory Model,它定義了主存、工作內(nèi)存抽象概念,底層對(duì)應(yīng)著 CPU 寄存器、緩存、硬件內(nèi)存、
CPU 指令優(yōu)化等。
JMM 體現(xiàn)在以下幾個(gè)方面
- 原子性 - 保證指令不會(huì)受到線程上下文切換的影響
- 可見性 - 保證指令不會(huì)受 cpu 緩存的影響
- 有序性 - 保證指令不會(huì)受 cpu 指令并行優(yōu)化的影響
可見性
退不出的循環(huán)
先來看一個(gè)現(xiàn)象,main 線程對(duì) run 變量的修改對(duì)于 t 線程不可見,導(dǎo)致了 t 線程無法停止:
static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....}});t.start();sleep(1);run = false; // 線程t不會(huì)如預(yù)想的停下來}
為什么呢?分析一下:
初始狀態(tài), t 線程剛開始從主內(nèi)存讀取了 run 的值到工作內(nèi)存。
因?yàn)?t 線程要頻繁從主內(nèi)存中讀取 run 的值,JIT 編譯器會(huì)將 run 的值緩存至自己工作內(nèi)存中的高速緩存中,
減少對(duì)主存中 run 的訪問,提高效率
1 秒之后,main 線程修改了 run 的值,并同步至主存,而 t 是從自己工作內(nèi)存中的高速緩存中讀取這個(gè)變量
的值,結(jié)果永遠(yuǎn)是舊值
解決方法
volatile(易變關(guān)鍵字)
它可以用來修飾成員變量和靜態(tài)成員變量,他可以避免線程從自己的工作緩存中查找變量的值,必須到主存中獲取
它的值,線程操作 volatile 變量都是直接操作主存
可見性 vs 原子性
前面例子體現(xiàn)的實(shí)際就是可見性,它保證的是在多個(gè)線程之間,一個(gè)線程對(duì) volatile 變量的修改對(duì)另一個(gè)線程可
見, 不能保證原子性,僅用在一個(gè)寫線程,多個(gè)讀線程的情況: 上例從字節(jié)碼理解是這樣的:
getstatic run // 線程 t 獲取 run true
getstatic run // 線程 t 獲取 run true
getstatic run // 線程 t 獲取 run true
getstatic run // 線程 t 獲取 run true
putstatic run // 線程 main 修改 run 為 false, 僅此一次
getstatic run // 線程 t 獲取 run false
比較一下之前我們將線程安全時(shí)舉的例子:兩個(gè)線程一個(gè) i++ 一個(gè) i-- ,只能保證看到最新值,不能解決指令交錯(cuò)
// 假設(shè)i的初始值為0
getstatic i // 線程2-獲取靜態(tài)變量i的值 線程內(nèi)i=0 getstatic i // 線程1-獲取靜態(tài)變量i的值 線程內(nèi)i=0
iconst_1 // 線程1-準(zhǔn)備常量1
iadd // 線程1-自增 線程內(nèi)i=1
putstatic i // 線程1-將修改后的值存入靜態(tài)變量i 靜態(tài)變量i=1 iconst_1 // 線程2-準(zhǔn)備常量1
isub // 線程2-自減 線程內(nèi)i=-1
putstatic i // 線程2-將修改后的值存入靜態(tài)變量i 靜態(tài)變量i=-1
注意:
- synchronized 語句塊既可以保證代碼塊的原子性,也同時(shí)保證代碼塊內(nèi)變量的可見性。但缺點(diǎn)是synchronized 是屬于重量級(jí)操作,性能相對(duì)更低。
synchronized 關(guān)鍵字可以確保多線程環(huán)境下的可見性,主要通過兩個(gè)方面來實(shí)現(xiàn):
互斥訪問:當(dāng)一個(gè)線程獲取到某個(gè)對(duì)象的鎖時(shí),其他線程無法同時(shí)獲取該對(duì)象的鎖,只能等待鎖釋放。這樣可以保證在同步塊中對(duì)共享變量的修改操作是原子的,不會(huì)被其他線程中斷。
內(nèi)存可見性:當(dāng)一個(gè)線程釋放鎖時(shí),會(huì)將對(duì)共享變量的修改刷新到主內(nèi)存中,而當(dāng)另一個(gè)線程獲取鎖時(shí),會(huì)從主內(nèi)存中重新讀取共享變量的值,確??吹阶钚碌闹?。
以下是一個(gè)例子來說明 synchronized 關(guān)鍵字如何保證可見性:
public class SynchronizedExample {private static boolean flag = false;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {synchronized (SynchronizedExample.class) {// 修改共享變量的值flag = true;System.out.println("Thread 1: flag is set to true");}});Thread thread2 = new Thread(() -> {// 暫停一段時(shí)間,確保 thread1 先執(zhí)行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (SynchronizedExample.class) {// 在同步塊中訪問共享變量if (flag) {System.out.println("Thread 2: flag is true");} else {System.out.println("Thread 2: flag is false");}}});thread1.start();thread2.start();thread1.join();thread2.join();
}
}
在這個(gè)例子中,有兩個(gè)線程 thread1 和 thread2,它們同時(shí)訪問了共享變量 flag。首先,thread1 獲取了 SynchronizedExample.class 對(duì)象的鎖,并將 flag 設(shè)置為 true,然后釋放鎖。接著,thread2 獲取了同一個(gè)鎖,并在同步塊中訪問 flag 的值。由于 thread2 獲取到了鎖并讀取了 flag 的最新值,因此能正確地判斷出 flag 的狀態(tài)。
通過 synchronized 關(guān)鍵字的互斥性和內(nèi)存可見性的特性,確保了多線程環(huán)境下的共享變量操作的一致性和可見性。
- 如果在前面示例的死循環(huán)中加入 System.out.println() 會(huì)發(fā)現(xiàn)即使不加 volatile 修飾符,線程 t 也能正確看到
對(duì) run 變量的修改了,想一想為什么?
static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....System.out.println();}});t.start();sleep(1);run = false; // 線程t不會(huì)如預(yù)想的停下來}
通過分析 System.out.println() 源碼來得出結(jié)果。
synchronized 是 Java 中用于實(shí)現(xiàn)同步的關(guān)鍵字,它可以用于修飾方法和代碼塊。當(dāng)一個(gè)線程獲取了對(duì)象的鎖,執(zhí)行 synchronized 修飾的代碼時(shí),會(huì)將變量從主內(nèi)存中拷貝到線程的本地內(nèi)存中。
在 synchronized 塊執(zhí)行結(jié)束后,JVM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中修改過的變量刷新回主內(nèi)存中,以保證不同線程間所共享的變量值的一致性。
模式之兩階段終止
// 停止標(biāo)記用 volatile 是為了保證該變量在多個(gè)線程之間的可見性
// 我們的例子中,即主線程把它修改為 true 對(duì) t1 線程可見
class TPTVolatile {private Thread thread;private volatile boolean stop = false;public void start(){thread = new Thread(() -> {while(true) {Thread current = Thread.currentThread();if(stop) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("將結(jié)果保存");} catch (InterruptedException e) {}// 執(zhí)行監(jiān)控操作}},"監(jiān)控線程");thread.start();}public void stop() {stop = true;thread.interrupt(); // 如果設(shè)置為ture后,線程還在sleep狀態(tài),那么使用打斷即可}
}
調(diào)用
TPTVolatile t = new TPTVolatile();
t.start();Thread.sleep(3500);
log.debug("stop");
t.stop();
結(jié)果
11:54:52.003 c.TPTVolatile [監(jiān)控線程] - 將結(jié)果保存
11:54:53.006 c.TPTVolatile [監(jiān)控線程] - 將結(jié)果保存
11:54:54.007 c.TPTVolatile [監(jiān)控線程] - 將結(jié)果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop
11:54:54.502 c.TPTVolatile [監(jiān)控線程] - 料理后事
同步模式之 Balking
定義
Balking (猶豫)模式用在一個(gè)線程發(fā)現(xiàn)另一個(gè)線程或本線程已經(jīng)做了某一件相同的事,那么本線程就無需再做
了,直接結(jié)束返回
實(shí)現(xiàn)
class TwoPhaseTermination {// 監(jiān)控線程private Thread monitorThread;// 停止標(biāo)記private volatile boolean stop = false;// 判斷是否執(zhí)行過 start 方法private boolean starting = false;// 啟動(dòng)監(jiān)控線程public void start() {synchronized (this) {if (starting) { // falsereturn;}starting = true;monitorThread = new Thread(() -> {while (true) {Thread current = Thread.currentThread();// 是否被打斷if (stop) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("執(zhí)行監(jiān)控記錄");} catch (InterruptedException e) {}}}, "monitor");monitorThread.start();}}// 停止監(jiān)控線程public void stop() {stop = true;monitorThread.interrupt();}
}public static void main(String[] args) throws InterruptedException {TwoPhaseTermination tpt = new TwoPhaseTermination();tpt.start();tpt.start();tpt.start();/*Thread.sleep(3500);log.debug("停止監(jiān)控");tpt.stop();*/}
當(dāng)前端頁面多次點(diǎn)擊按鈕調(diào)用 start 時(shí)
輸出
[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 該監(jiān)控線程已啟動(dòng)?(false)
[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 監(jiān)控線程已啟動(dòng)...
[http-nio-8080-exec-2] cn.itcast.monitor.service.MonitorService - 該監(jiān)控線程已啟動(dòng)?(true)
[http-nio-8080-exec-3] cn.itcast.monitor.service.MonitorService - 該監(jiān)控線程已啟動(dòng)?(true)
[http-nio-8080-exec-4] cn.itcast.monitor.service.MonitorService - 該監(jiān)控線程已啟動(dòng)?(true)
一個(gè)優(yōu)化就是 盡可能的讓synchronized代碼塊中的代碼較少,所以可以將不關(guān)鍵的因素抽取出來
public void start() {synchronized (this) {if (starting) { // falsereturn;}starting = true;}monitorThread = new Thread(() -> {while (true) {Thread current = Thread.currentThread();// 是否被打斷if (stop) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("執(zhí)行監(jiān)控記錄");} catch (InterruptedException e) {}}}, "monitor");monitorThread.start();}
它還經(jīng)常用來實(shí)現(xiàn)線程安全的單例
public final class Singleton {private Singleton() {}private static Singleton INSTANCE = null;public static synchronized Singleton getInstance() {if (INSTANCE != null) {return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}
}
有序性
JVM 會(huì)在不影響正確性的前提下,可以調(diào)整語句的執(zhí)行順序,思考下面一段代碼
static int i;
static int j;// 在某個(gè)線程內(nèi)執(zhí)行如下賦值操作
i = ...;
j = ...;
可以看到,至于是先執(zhí)行 i 還是 先執(zhí)行 j ,對(duì)最終的結(jié)果不會(huì)產(chǎn)生影響。所以,上面代碼真正執(zhí)行時(shí),既可以是
i = ...;
j = ...;
也可以是
j = ...;
i = ...;
這種特性稱之為『指令重排』,多線程下『指令重排』會(huì)影響正確性。為什么要有重排指令這項(xiàng)優(yōu)化呢?從 CPU
執(zhí)行指令的原理來理解一下吧
指令級(jí)并行原理
名詞
Clock Cycle Time
主頻的概念大家接觸的比較多,而 CPU 的 Clock Cycle Time(時(shí)鐘周期時(shí)間),等于主頻的倒數(shù),意思是 CPU 能
夠識(shí)別的最小時(shí)間單位,比如說 4G 主頻的 CPU 的 Clock Cycle Time 就是 0.25 ns,作為對(duì)比,我們墻上掛鐘的
Cycle Time 是 1s
例如,運(yùn)行一條加法指令一般需要一個(gè)時(shí)鐘周期時(shí)間
CPI
有的指令需要更多的時(shí)鐘周期時(shí)間,所以引出了 CPI (Cycles Per Instruction)指令平均時(shí)鐘周期數(shù)
IPC
IPC(Instruction Per Clock Cycle) 即 CPI 的倒數(shù),表示每個(gè)時(shí)鐘周期能夠運(yùn)行的指令數(shù)
CPU 執(zhí)行時(shí)間
程序的 CPU 執(zhí)行時(shí)間,即我們前面提到的 user + system 時(shí)間,可以用下面的公式來表示
程序 CPU 執(zhí)行時(shí)間 = 指令數(shù) * CPI * Clock Cycle Time
魚罐頭的故事
加工一條魚需要 50 分鐘,只能一條魚、一條魚順序加工…
可以將每個(gè)魚罐頭的加工流程細(xì)分為 5 個(gè)步驟:
- 去鱗清洗 10分鐘
- 蒸煮瀝水 10分鐘
- 加注湯料 10分鐘
- 殺菌出鍋 10分鐘
- 真空封罐 10分鐘
即使只有一個(gè)工人,最理想的情況是:他能夠在 10 分鐘內(nèi)同時(shí)做好這 5 件事,因?yàn)閷?duì)第一條魚的真空裝罐,不會(huì)
影響對(duì)第二條魚的殺菌出鍋…
指令重排序優(yōu)化
事實(shí)上,現(xiàn)代處理器會(huì)設(shè)計(jì)為一個(gè)時(shí)鐘周期完成一條執(zhí)行時(shí)間最長的 CPU 指令。為什么這么做呢?可以想到指令
還可以再劃分成一個(gè)個(gè)更小的階段,例如,每條指令都可以分為: 取指令 - 指令譯碼 - 執(zhí)行指令 - 內(nèi)存訪問 - 數(shù)據(jù)
寫回 這 5 個(gè)階段
在不改變程序結(jié)果的前提下,這些指令的各個(gè)階段可以通過重排序和組合來實(shí)現(xiàn)指令級(jí)并行,這一技術(shù)在 80’s 中
葉到 90’s 中葉占據(jù)了計(jì)算架構(gòu)的重要地位。(分階段,分工是提升效率的關(guān)鍵!)
指令重排的前提是,重排指令不能影響結(jié)果,例如
// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2
支持流水線的處理器
現(xiàn)代 CPU 支持多級(jí)指令流水線,例如支持同時(shí)執(zhí)行 取指令 - 指令譯碼 - 執(zhí)行指令 - 內(nèi)存訪問 - 數(shù)據(jù)寫回 的處理
器,就可以稱之為五級(jí)指令流水線。這時(shí) CPU 可以在一個(gè)時(shí)鐘周期內(nèi),同時(shí)運(yùn)行五條指令的不同階段(相當(dāng)于一
條執(zhí)行時(shí)間最長的復(fù)雜指令),IPC = 1,本質(zhì)上,流水線技術(shù)并不能縮短單條指令的執(zhí)行時(shí)間,但它變相地提高了指令地吞吐率。(奔騰四(Pentium 4)支持高達(dá) 35 級(jí)流水線,但由于功耗太高被廢棄)
SuperScalar 處理器
大多數(shù)處理器包含多個(gè)執(zhí)行單元,并不是所有計(jì)算功能都集中在一起,可以再細(xì)分為整數(shù)運(yùn)算單元、浮點(diǎn)數(shù)運(yùn)算單
元等,這樣可以把多條指令也可以做到并行獲取、譯碼等,CPU 可以在一個(gè)時(shí)鐘周期內(nèi),執(zhí)行多于一條指令,IPC>1
詭異的結(jié)果
int num = 0;boolean ready = false;// 線程1 執(zhí)行此方法public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}// 線程2 執(zhí)行此方法public void actor2(I_Result r) {num = 2;ready = true;}
有一個(gè)屬性 r1 用來保存結(jié)果,問,可能的結(jié)果有幾種?
有同學(xué)這么分析
- 情況1:線程1 先執(zhí)行,這時(shí) ready = false,所以進(jìn)入 else 分支結(jié)果為 1
- 情況2:線程2 先執(zhí)行 num = 2,但沒來得及執(zhí)行 ready = true,線程1 執(zhí)行,還是進(jìn)入 else 分支,結(jié)果為1
- 情況3:線程2 執(zhí)行到 ready = true,線程1 執(zhí)行,這回進(jìn)入 if 分支,結(jié)果為 4(因?yàn)?num 已經(jīng)執(zhí)行過了)
但我告訴你,結(jié)果還有可能是 0 😁😁😁,信不信吧!
這種情況下是:線程2 執(zhí)行 ready = true,切換到線程1,進(jìn)入 if 分支,相加為 0,再切回線程2 執(zhí)行 num = 2
相信很多人已經(jīng)暈了 😵😵(原因就是jvm對(duì)這 num 和 ready進(jìn)行了指令重排序,那么結(jié)果可能是0)
這種現(xiàn)象叫做指令重排,是 JIT 編譯器在運(yùn)行時(shí)的一些優(yōu)化,這個(gè)現(xiàn)象需要通過大量測(cè)試才能復(fù)現(xiàn):
借助 java 并發(fā)壓測(cè)工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -
DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -DartifactId=ordering -Dversion=1.0
創(chuàng)建 maven 項(xiàng)目,提供如下測(cè)試類
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {int num = 0;boolean ready = false;@Actorpublic void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}@Actorpublic void actor2(I_Result r) {num = 2;ready = true;}
}
執(zhí)行
mvn clean install
java -jar target/jcstress.jar
會(huì)輸出我們感興趣的結(jié)果,摘錄其中一次結(jié)果:
*** INTERESTING testsSome interesting behaviors observed. This is for the plain curiosity.2 matching test results.[OK] test.ConcurrencyTest(JVM args: [-XX:-TieredCompilation])Observed state Occurrences Expectation Interpretation0 1,729 ACCEPTABLE_INTERESTING !!!!1 42,617,915 ACCEPTABLE ok4 5,146,627 ACCEPTABLE ok[OK] test.ConcurrencyTest(JVM args: [])Observed state Occurrences Expectation Interpretation0 1,652 ACCEPTABLE_INTERESTING !!!!1 46,460,657 ACCEPTABLE ok4 4,571,072 ACCEPTABLE ok
可以看到,出現(xiàn)結(jié)果為 0 的情況有 一千多次,雖然次數(shù)相對(duì)很少,但畢竟是出現(xiàn)了。
解決方法
volatile 修飾的變量,可以禁用指令重排
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {int num = 0;volatile boolean ready = false;@Actorpublic void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}@Actorpublic void actor2(I_Result r) {num = 2;ready = true;// 在這上面加 volatile 能夠防止 之前的代碼被重排序}
}
結(jié)果為:
*** INTERESTING tests Some interesting behaviors observed. This is for the plain curiosity. 0 matching test results.
volatile 原理
volatile 的底層實(shí)現(xiàn)原理是內(nèi)存屏障,Memory Barrier(Memory Fence)
- 對(duì) volatile 變量的寫指令后會(huì)加入寫屏障
- 對(duì) volatile 變量的讀指令前會(huì)加入讀屏障
如何保證可見性
寫屏障(sfence)保證在該屏障之前的,對(duì)共享變量的改動(dòng),都同步到主存當(dāng)中
public void actor2(I_Result r) {num = 2;ready = true; // ready 是 volatile 賦值帶寫屏障// 寫屏障}
而讀屏障(lfence)保證在該屏障之后,對(duì)共享變量的讀取,加載的是主存中最新數(shù)據(jù)
public void actor1(I_Result r) {// 讀屏障// ready 是 volatile 讀取值帶讀屏障if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}
如何保證有序性
- 寫屏障會(huì)確保指令重排序時(shí),不會(huì)將寫屏障之前的代碼排在寫屏障之后
- 讀屏障會(huì)確保指令重排序時(shí),不會(huì)將讀屏障之后的代碼排在讀屏障之前
還是那句話,不能解決指令交錯(cuò):
- 寫屏障僅僅是保證之后的讀能夠讀到最新的結(jié)果,但不能保證讀跑到它前面去
- 而有序性的保證也只是保證了本線程內(nèi)相關(guān)代碼不被重排序
volatile底層的讀寫屏障只是 保證了可見性 和 有序性,還是不能解決指令交錯(cuò)
而synchronized都可以做到,有序 可見 原子
double-checked locking 問題
以著名的 double-checked locking 單例模式為例
public final class Singleton {private Singleton() { }private static Singleton INSTANCE = null;public static Singleton getInstance() {if(INSTANCE == null) { // t2// 首次訪問會(huì)同步,而之后的使用沒有 synchronizedsynchronized(Singleton.class) {if (INSTANCE == null) { // t1INSTANCE = new Singleton();}}}return INSTANCE;}
}
以上的實(shí)現(xiàn)特點(diǎn)是:
- 懶惰實(shí)例化
- 首次使用 getInstance() 才使用 synchronized 加鎖,后續(xù)使用時(shí)無需加鎖
- 有隱含的,但很關(guān)鍵的一點(diǎn):第一個(gè) if 使用了 INSTANCE 變量,是在同步塊之外
但在多線程環(huán)境下,上面的代碼是有問題的,getInstance 方法對(duì)應(yīng)的字節(jié)碼為:
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
其中
- 17 表示創(chuàng)建對(duì)象,將對(duì)象引用入棧 // new Singleton
- 20 表示復(fù)制一份對(duì)象引用 // 引用地址
- 21 表示利用一個(gè)對(duì)象引用,調(diào)用構(gòu)造方法
- 24 表示利用一個(gè)對(duì)象引用,賦值給 static INSTANCE
也許 jvm 會(huì)優(yōu)化為:先執(zhí)行 24,再執(zhí)行 21。如果兩個(gè)線程 t1,t2 按如下時(shí)間序列執(zhí)行:
關(guān)鍵在于 0: getstatic 這行代碼在 monitor 控制之外,它就像之前舉例中不守規(guī)則的人,可以越過 monitor 讀取
INSTANCE 變量的值。
這時(shí) t1 還未完全將構(gòu)造方法執(zhí)行完畢,如果在構(gòu)造方法中要執(zhí)行很多初始化操作,那么 t2 拿到的是將是一個(gè)未初
始化完畢的單例。
對(duì) INSTANCE 使用 volatile 修飾即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才會(huì)真正有效
這里面,前面介紹的synchronized不是太嚴(yán)謹(jǐn) 。能保證 原子 可見 有序
synchronized仍然是可以被重排序的,并不能組織重排序,volatile才能組織重排序,但是如果共享變量完全被synchronized 所保護(hù),那么共享變量在使用的過程中是不會(huì)有 原子 可見 有序問題的,就算中間發(fā)生了重排序,但是只要完全交給synchronized管理,是不會(huì)有有序性問題的。
剛才使用出現(xiàn)問題,是因?yàn)楣蚕碜兞坎]有完全的被synchronized保護(hù)起來,synchronized外面還有共享變量的使用
double-checked locking 解決
public final class Singleton {private Singleton() { }private static volatile Singleton INSTANCE = null;public static Singleton getInstance() {// 實(shí)例沒創(chuàng)建,才會(huì)進(jìn)入內(nèi)部的 synchronized代碼塊if (INSTANCE == null) {synchronized (Singleton.class) { // t2// 也許有其它線程已經(jīng)創(chuàng)建實(shí)例,所以再判斷一次if (INSTANCE == null) { // t1INSTANCE = new Singleton();}}}return INSTANCE;}
}
字節(jié)碼上看不出來 volatile 指令的效果
如上面的注釋內(nèi)容所示,讀寫 volatile 變量時(shí)會(huì)加入內(nèi)存屏障(Memory Barrier(Memory Fence)),保證下面兩點(diǎn):
可見性
寫屏障(sfence)保證在該屏障之前的 t1 對(duì)共享變量的改動(dòng),都同步到主存當(dāng)中
而讀屏障(lfence)保證在該屏障之后 t2 對(duì)共享變量的讀取,加載的是主存中最新數(shù)據(jù)
有序性
寫屏障會(huì)確保指令重排序時(shí),不會(huì)將寫屏障之前的代碼排在寫屏障之后
讀屏障會(huì)確保指令重排序時(shí),不會(huì)將讀屏障之后的代碼排在讀屏障之前
happens-before
happens-before 規(guī)定了對(duì)共享變量的寫操作對(duì)其它線程的讀操作可見,它是可見性與有序性的一套規(guī)則總結(jié),拋
開以下 happens-before 規(guī)則,JMM 并不能保證一個(gè)線程對(duì)共享變量的寫,對(duì)于其它線程對(duì)該共享變量的讀可見
線程解鎖 m 之前對(duì)變量的寫,對(duì)于接下來對(duì) m 加鎖的其它線程對(duì)該變量的讀可見
static int x;static Object m = new Object();new Thread(()->{synchronized(m) {x = 10;}},"t1").start();new Thread(()->{synchronized(m) {System.out.println(x);}},"t2").start();
線程對(duì) volatile 變量的寫,對(duì)接下來其它線程對(duì)該變量的讀可見
volatile static int x;new Thread(()->{x = 10;
},"t1").start();new Thread(()->{System.out.println(x);
},"t2").start();
線程 start 前對(duì)變量的寫,對(duì)該線程開始后對(duì)該變量的讀可見
static int x;x = 10;new Thread(()->{System.out.println(x);
},"t2").start();
線程結(jié)束前對(duì)變量的寫,對(duì)其它線程得知它結(jié)束后的讀可見(比如其它線程調(diào)用 t1.isAlive() 或 t1.join()等待它結(jié)束)
static int x;Thread t1 = new Thread(()->{x = 10;
},"t1");t1.start();
t1.join();
System.out.println(x);
線程 t1 打斷 t2(interrupt)前對(duì)變量的寫,對(duì)于其他線程得知 t2 被打斷后對(duì)變量的讀可見(通過t2.interrupted 或 t2.isInterrupted)
Thread t2 = new Thread(()->{while(true) {if(Thread.currentThread().isInterrupted()) {System.out.println(x);break;}}},"t2");t2.start();new Thread(()->{try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}x = 10;t2.interrupt();},"t1").start();
具有傳遞性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
volatile static int x;
static int y;
new Thread(()->{ y = 10;x = 20;
},"t1").start();
new Thread(()->{// x=20 對(duì) t2 可見, 同時(shí) y=10 也對(duì) t2 可見System.out.println(x);
},"t2").start();
主要是 寫屏障之前都同步到主內(nèi)存
balking 模式習(xí)題
希望 doInit() 方法僅被調(diào)用一次,下面的實(shí)現(xiàn)是否有問題,為什么?
public class TestVolatile {volatile boolean initialized = false;void init() {if (initialized) {return;}doInit();initialized = true;}private void doInit() {}
}
其實(shí)這里,在多線程的情況下,因?yàn)闆]有保證多線程之間的原子性,所以會(huì)出現(xiàn)問題。
線程安全單例習(xí)題
單例模式有很多實(shí)現(xiàn)方法,餓漢、懶漢、靜態(tài)內(nèi)部類、枚舉類,試分析每種實(shí)現(xiàn)下獲取單例對(duì)象(即調(diào)用
getInstance)時(shí)的線程安全,并思考注釋中的問題
- 餓漢式:類加載就會(huì)導(dǎo)致該單實(shí)例對(duì)象被創(chuàng)建
- 懶漢式:類加載不會(huì)導(dǎo)致該單實(shí)例對(duì)象被創(chuàng)建,而是首次使用該對(duì)象時(shí)才會(huì)創(chuàng)建
實(shí)現(xiàn)1
// 問題1:為什么加 final
// 問題2:如果實(shí)現(xiàn)了序列化接口, 還要做什么來防止反序列化破壞單例
public final class Singleton implements Serializable {// 問題3:為什么設(shè)置為私有? 是否能防止反射創(chuàng)建新的實(shí)例?private Singleton() {}// 問題4:這樣初始化是否能保證單例對(duì)象創(chuàng)建時(shí)的線程安全?private static final Singleton INSTANCE = new Singleton();// 問題5:為什么提供靜態(tài)方法而不是直接將 INSTANCE 設(shè)置為 public, 說出你知道的理由public static Singleton getInstance() {return INSTANCE;}public Object readResolve() {return INSTANCE;}
}
問題1:怕將來有子類,覆蓋某些方法,破壞了單例
問題2:反序列化維護(hù)的對(duì)象,和單例維護(hù)的對(duì)象不一樣,因?yàn)?需要加上
public Obejct readResolve(){
return INSTANCE;
}
反序列化的過程中,一旦發(fā)現(xiàn)了readResolve 返回的對(duì)象,就會(huì)用你返回的對(duì)象,而不是反序列化字節(jié)碼生成的對(duì)象。
問題3: 設(shè)置成public,別的類能夠無限的創(chuàng)建對(duì)象了,并不能夠防止反射
問題4: 靜態(tài)成員變量,初始化操作是在類加載的時(shí)候。類加載階段是由jvm來保證這些代碼的線程安全性。所以類加載階段做成員賦值都是線程安全的。
問題5: 用方法說明提供了更好的封裝性,可以內(nèi)部實(shí)現(xiàn)懶惰的初始化。還可以對(duì)創(chuàng)建的時(shí)間有更多的控制。
實(shí)現(xiàn)2(※※※重點(diǎn)難點(diǎn)※※※)
// 問題1:枚舉單例是如何限制實(shí)例個(gè)數(shù)的
// 問題2:枚舉單例在創(chuàng)建時(shí)是否有并發(fā)問題
// 問題3:枚舉單例能否被反射破壞單例
// 問題4:枚舉單例能否被反序列化破壞單例
// 問題5:枚舉單例屬于懶漢式還是餓漢式
// 問題6:枚舉單例如果希望加入一些單例創(chuàng)建時(shí)的初始化邏輯該如何做
enum Singleton {INSTANCE;
}
問題1: 枚舉單例是一種實(shí)現(xiàn)單例模式的方式,它通過使用枚舉類型來限制實(shí)例個(gè)數(shù)為一個(gè)。在Java中,枚舉類型是保證全局唯一的,因此使用枚舉來實(shí)現(xiàn)單例可以有效地避免多線程環(huán)境下的并發(fā)問題,并且在序列化和反序列化中也可以得到保證。
問題2: 是靜態(tài)成員變量,類加載階段完成的,不會(huì)有并發(fā)問題
問題3: 枚舉單例是一種實(shí)現(xiàn)單例模式的有效方式,因?yàn)樗梢员苊馔ㄟ^反射和序列化等方式破壞單例。枚舉類型在Java中是天然的單例,只能在JVM中被實(shí)例化一次。當(dāng)使用反射來訪問枚舉類型時(shí),JVM會(huì)保證每個(gè)枚舉類型只被實(shí)例化一次。這是因?yàn)槊杜e類型在Java中是特殊的類,由JVM特別處理。因此,即使使用反射來訪問枚舉類型并試圖創(chuàng)建一個(gè)新的實(shí)例,JVM也會(huì)返回已經(jīng)存在的單例。因此,枚舉單例不能被反射破壞單例。
問題4: 枚舉單例在Java中也可以防止被反序列化破壞單例。當(dāng)一個(gè)枚舉類型被序列化并再次反序列化時(shí),JVM會(huì)自動(dòng)確保只存在一個(gè)實(shí)例。這是因?yàn)槊杜e類型的序列化和反序列化是由JVM處理的,并且JVM會(huì)保證對(duì)于同一個(gè)枚舉類型只有一個(gè)實(shí)例。因此,嘗試通過反序列化來破壞枚舉單例是無效的,反序列化操作會(huì)返回已經(jīng)存在的單例實(shí)例,而不會(huì)創(chuàng)建新的實(shí)例??傊?#xff0c;枚舉單例是一種安全且可靠的單例實(shí)現(xiàn)方式,即使在面對(duì)反射和序列化等特性時(shí)也能保持單例的完整性。
問題5: 餓漢式
問題6: 如果希望在枚舉單例創(chuàng)建時(shí)加入一些初始化邏輯,可以在枚舉中定義一個(gè)構(gòu)造函數(shù),并將初始化邏輯放在其中
public enum SingletonEnum {INSTANCE;private SingletonEnum() {// 這里可以加入單例創(chuàng)建時(shí)的初始化邏輯System.out.println("SingletonEnum has been initialized.");}// 其它方法
}
在上面的示例中,我們定義了一個(gè)名為SingletonEnum的枚舉,其中INSTANCE是該枚舉的唯一實(shí)例。同時(shí),我們定義了一個(gè)構(gòu)造函數(shù)來進(jìn)行單例的初始化邏輯。
此處需要注意,在枚舉類型中,構(gòu)造函數(shù)必須是私有的。這是因?yàn)镴ava語言規(guī)范規(guī)定,只能在枚舉類型內(nèi)部定義枚舉常量,而枚舉常量的創(chuàng)建是由編譯器自動(dòng)生成的。因此,枚舉類型的構(gòu)造函數(shù)必須是私有的,以確保只有編譯器才能調(diào)用它。
實(shí)現(xiàn)3:
public final class Singleton {private Singleton() { }private static Singleton INSTANCE = null;// 分析這里的線程安全, 并說明有什么缺點(diǎn)public static synchronized Singleton getInstance() {if( INSTANCE != null ){return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}
}
性能問題:
由于synchronized關(guān)鍵字鎖住的是整個(gè)方法,在多線程高并發(fā)的情況下,可能會(huì)導(dǎo)致性能下降。每次調(diào)用getInstance()時(shí)都需要獲取鎖,即使實(shí)例已經(jīng)被創(chuàng)建。
實(shí)例提前創(chuàng)建:在多線程環(huán)境下,如果有一個(gè)線程獲取鎖并創(chuàng)建了實(shí)例,其它線程進(jìn)入方法后會(huì)發(fā)現(xiàn)實(shí)例已被創(chuàng)建,但仍需要等待鎖的釋放才能繼續(xù)執(zhí)行。這樣就造成了實(shí)例的提前創(chuàng)建,占用了內(nèi)存資源。
實(shí)現(xiàn)4:DCL
public final class Singleton {private Singleton() { }// 問題1:解釋為什么要加 volatile ?private static volatile Singleton INSTANCE = null;// 問題2:對(duì)比實(shí)現(xiàn)3, 說出這樣做的意義 public static Singleton getInstance() {if (INSTANCE != null) {return INSTANCE;}synchronized (Singleton.class) {// 問題3:為什么還要在這里加為空判斷, 之前不是判斷過了嗎if (INSTANCE != null) { // t2 return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}}
}
問題1: 保障了可見性 和 有序性
可見性
- 寫屏障(sfence)保證在該屏障之前的 t1 對(duì)共享變量的改動(dòng),都同步到主存當(dāng)中
- 而讀屏障(lfence)保證在該屏障之后 t2 對(duì)共享變量的讀取,加載的是主存中最新數(shù)據(jù)
有序性
- 寫屏障會(huì)確保指令重排序時(shí),不會(huì)將寫屏障之前的代碼排在寫屏障之后
- 讀屏障會(huì)確保指令重排序時(shí),不會(huì)將讀屏障之后的代碼排在讀屏障之前
問題2: 這種方式在第一次檢查時(shí)可以避免不必要的鎖競爭,提高了性能。并且通過使用volatile關(guān)鍵字修飾實(shí)例變量,保證了線程之間對(duì)實(shí)例的可見性,確保正確的初始化。這樣既滿足了線程安全性,又提高了性能。
問題3: 其實(shí)是為了防止最一開始同時(shí)有多個(gè)線程 繞過了第一個(gè) 不等于 null的判斷
實(shí)現(xiàn)5
public final class Singleton {private Singleton() { }// 問題1:屬于懶漢式還是餓漢式private static class LazyHolder {static final Singleton INSTANCE = new Singleton();}// 問題2:在創(chuàng)建時(shí)是否有并發(fā)問題public static Singleton getInstance() {return LazyHolder.INSTANCE;}
}
這段代碼使用了靜態(tài)內(nèi)部類的方式實(shí)現(xiàn)了單例模式,常被稱為“靜態(tài)內(nèi)部類單例模式”。
屬于懶漢式還是餓漢式?
該實(shí)現(xiàn)方式屬于懶漢式,因?yàn)閷?shí)例對(duì)象的創(chuàng)建發(fā)生在靜態(tài)內(nèi)部類LazyHolder被調(diào)用時(shí)。而不是在類加載時(shí)就創(chuàng)建實(shí)例對(duì)象。
在創(chuàng)建時(shí)是否有并發(fā)問題?
由于靜態(tài)內(nèi)部類LazyHolder在類加載時(shí)并不會(huì)被初始化,只有當(dāng)getInstance()方法被調(diào)用時(shí)才會(huì)加載,因此不存在并發(fā)創(chuàng)建實(shí)例的問題。
同時(shí),由于Java虛擬機(jī)在加載類時(shí)會(huì)對(duì)類進(jìn)行加鎖,所以多線程同時(shí)加載Singleton類也不會(huì)導(dǎo)致并發(fā)問題。
這種實(shí)現(xiàn)方式同時(shí)具備了懶漢式和餓漢式的優(yōu)點(diǎn)。在需要使用實(shí)例對(duì)象時(shí)才會(huì)進(jìn)行實(shí)例化,避免了餓漢式可能造成的資源浪費(fèi);同時(shí)在加載靜態(tài)內(nèi)部類時(shí),JVM會(huì)自動(dòng)加鎖,保證了線程安全性。因此,該實(shí)現(xiàn)方式既滿足了線程安全性,又提高了性能和資源利用率。