目錄
- 1.Java記憶體模型概述
- 2.原子性
- 2.1.問題提出
- 2.2.問題分析
- 2.3.解決辦法
- 3.原子性
- 3.1.退不出的回圈
- 3.2 解決方法
- 3.3 可見性
- 4.有序性
- 4.1.詭異的結果
- 4.2.解決辦法
- 4.3.有序性理解
- 5. happens-before
\
1.Java記憶體模型概述
很多人將【java 記憶體結構】與【java 記憶體模型】傻傻分不清,【java 記憶體模型】是 Java Memory Model(JMM)的意思,
關于它的權威解釋,請參考 https://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfdspec-oth-JSpec/memory_model-1_0-pfd-spec.pdf? AuthParam=1562811549_4d4994cbd5b59d964cd2907ea22ca08b,
簡單的說,JMM 定義了一套在多執行緒讀寫共享資料時(成員變數、陣列)時,對資料的可見性、有序 性、和原子性的規則和保障
MM 即 Java Memory Model,它定義了主存(共享記憶體)、作業記憶體(執行緒私有)抽象概念,底層對應著 CPU 暫存器、快取、硬體記憶體、 CPU 指令優化等,
JMM體現在以下幾個方面
原子性 - 保證指令不會受到執行緒背景關系切換的影響
可見性 - 保證指令不會受 cpu 快取的影響
有序性 - 保證指令不會受 cpu 指令并行優化的影響
2.原子性
2.1.問題提出
原子性在學習執行緒時講過,下面來個例子簡單回顧一下:
問題提出,兩個執行緒對初始值為 0 的靜態變數一個做自增,一個做自減,各做 5000 次,結果是 0 嗎?
答案是結果不一定是0
public class Demo4_1 {
static int i = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
i++;
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
i--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
運行結果

2.2.問題分析
以上的結果可能是正數、負數、零,為什么呢?因為 Java 中對靜態變數的自增,自減并不是原子操作,
例如對于 i++ 而言(i 為靜態變數),實際會產生如下的 JVM 位元組碼指令:
getstatic i // 獲取靜態變數i的值
iconst_1 // 準備常量1
iadd // 加法
putstatic i // 將修改后的值存入靜態變數i
而對應 i-- 也是類似:
getstatic i // 獲取靜態變數i的值
iconst_1 // 準備常量1
isub // 減法
putstatic i // 將修改后的值存入靜態變數i
注意,這里靜態變數和區域變數取數時所用的指令不同,區域變數是在區域變數曹上執行的,
**而java的記憶體模型如下,記憶體模型把整個記憶體分為兩部分,一部分叫主記憶體,一部分叫作業記憶體,它們不能和記憶體結構中的堆疊相混淆,
完成靜態變數的自增、自減需要在主存和執行緒記憶體中進行資料交換,
getstatic把主存中把i的值讀入到作業記憶體中來,然后作業記憶體中做完了加法,然后把結果寫回到主記憶體中去,

如果是單執行緒以上 8 行代碼是順序執行(不會交錯)沒有問題:
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改后的值存入靜態變數i 靜態變數i=1
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=1
iconst_1 // 執行緒1-準備常量1
isub // 執行緒1-自減 執行緒內i=0
putstatic i // 執行緒1-將修改后的值存入靜態變數i 靜態變數i=0
但多執行緒下這 8 行代碼可能交錯運行(為什么會交錯?思考一下): 出現負數的情況:
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改后的值存入靜態變數i 靜態變數i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改后的值存入靜態變數i 靜態變數i=-1
整個作業系統中,在微觀上看,是一種多任務性的,執行緒會輪流拿到CPU的時間片,有可能CPU在時間片1的時候執行執行緒1,在時間片2的時候執行執行緒2,但總是會出現這樣一種情況,CPU執行執行緒1getstatic,獲取到了靜態變數的初始值,剛開始i為0,但是在這個時候,時間片用完,CPU執行執行緒2的代碼,也是執行getstatic,此時獲取到的初始值也為0.這樣在后續的操作中,它們都是對0做加減,最終執行緒2的結果覆寫了執行緒1的結果,我們得到的結果是-1,
出現正數的情況:
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改后的值存入靜態變數i 靜態變數i=-1
putstatic i // 執行緒1-將修改后的值存入靜態變數i 靜態變數i=1
2.3.解決辦法
synchronized同步關鍵字
語法
synchronized( 物件 ) {
要作為原子操作代碼
}
當某個執行緒執行到synchronized陳述句并獲取到了鎖,然后就可以順利的執行synchronized中的同步代碼塊,其它執行緒想執行這里的原子操作代碼就必須要等到那個執行緒執行完并釋放鎖,這樣就保證了同步代碼塊內的原子性,
用 synchronized 解決并發問題:
static int i = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
synchronized (obj) {
i++;
}
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
synchronized (obj) {
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}

如何理解呢:你可以把 obj 想象成一個房間,執行緒 t1,t2 想象成兩個人,
當執行緒 t1 執行到 synchronized(obj) 時就好比 t1 進入了這個房間,并反手鎖住了門,在門內執行 count++ 代碼, 這時候如果 t2 也運行到了 synchronized(obj) 時,它發現門被鎖住了,只能在門外等待,
當 t1 執行完 synchronized{} 塊內的代碼,這時候才會解開門上的鎖,從 obj 房間出來,t2 執行緒這時才 可以進入 obj 房間,反鎖住門,執行它的 count-- 代碼,
注意:上例中 t1 和 t2 執行緒必須用 synchronized 鎖住同一個 obj 物件,如果 t1 鎖住的是 m1 對 象,t2 鎖住的是 m2 物件,就好比兩個人分別進入了兩個不同的房間,沒法起到同步的效果,
專業一點的如下

當一個執行緒t1來了,如果它發現monitor中的owner是空著的,并沒有其它執行緒所占據,這個時候,這個執行緒就會成為owner,并且會用monitor_enter指令對monitor進行鎖定,
當第二個執行緒t2來了,發現t1已經成為了owner,并且把monitor鎖定了,但是它可以進入EntryList,這是一個排隊等候區,它實際上阻塞了,
當t1執行完了之后,它會執行一個monitor_exit,它會通知entryList中正在等待的執行緒,這個時候t2執行緒就可以成為新的owner,然后執行同樣的程序,
如果EntryList有多個執行緒的話,它們實際上會爭搶成為新的owner
但是這樣效率會非常低,我們執行50000次,這樣就要執行50000次的monitor_enter和monitor_exit指令,這樣是比較耗時的,雖然Java虛擬機支持對同一個物件monitor_enter和exit,顯然效率比較低,
3.原子性
3.1.退不出的回圈
先來看一個現象,main 執行緒對 run 變數的修改對于 t 執行緒不可見,導致了 t 執行緒無法停止:
public class Demo4_2 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
System.out.println(1);
}
});
t.start();
Thread.sleep(1000);
run = false; // 執行緒t不會如預想的停下來
}
}
為什么呢?分析一下:
- 初始狀態, t 執行緒剛開始從主記憶體讀取了 run 的值到作業記憶體,

- 因為 t 執行緒要頻繁從主記憶體中讀取 run 的值,JIT 編譯器會將 run 的值快取至自己作業記憶體中的高速快取中,減少對主存中 run 的訪問,提高效率

- 1 秒之后,main 執行緒修改了 run 的值,并同步至主存,而 t 是從自己作業記憶體中的高速快取中讀 取這個變數的值,結果永遠是舊值

3.2 解決方法
volatile(易變關鍵字)
它可以用來修飾成員變數和靜態成員變數,他可以避免執行緒從自己的作業快取中查找變數的值,必須到 主存中獲取它的值,執行緒操作 volatile 變數都是直接操作主存
public class Demo4_2 {
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; // 執行緒t不會如預想的停下來
}
}
此時,程式正常運行結束,
因為volatile修飾的變數,讀取是每次都到主存中讀取的,不會說每次都到高速快取中讀取,這樣就保證了讀取的執行緒看到的總是最新的結果,
3.3 可見性
前面例子體現的實際就是可見性,它保證的是在多個執行緒之間,一個執行緒對 volatile 變數的修改對另一 個執行緒可見, 不能保證原子性,僅用在一個寫執行緒,多個讀執行緒的情況:
上例從位元組碼理解是這樣的:
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
比較一下之前我們將執行緒安全時舉的例子:兩個執行緒一個 i++ 一個 i-- ,只能保證看到新值,不能解 決指令交錯
//假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改后的值存入靜態變數i 靜態變數i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改后的值存入靜態變數i 靜態變數i=-
可見性與原子性
注意 synchronized 陳述句塊既可以保證代碼塊的原子性,也同時保證代碼塊內變數的可見性,
但缺點是 synchronized 是屬于重量級操作,性能相對更低,
如果在前面示例的死回圈中加入 System.out.println() 會發現即使不加 volatile 修飾符,執行緒 t 也能正確看到 對 run 變數的修改了,想一想為什么?
public class Demo4_2 {
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
System.out.println(1);
}
});
t.start();
Thread.sleep(1000);
run = false; // 執行緒t不會如預想的停下來
}
}
這是因為底層synchronized方法起到了作用,
public void println(String x) {
//使用了synchronized關鍵字
synchronized (this) {
print(x);
newLine();
}
}
因為它也可以防止當前執行緒從高速快取中獲取的值,破壞了剛才的JIT的優化,
4.有序性
4.1.詭異的結果
int num = 0;
boolean ready = false;
// 執行緒1 執行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 執行緒2 執行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
I_Result 是一個物件,有一個屬性 r1 用來保存結果,問,可能的結果有幾種?
有同學這么分析
情況1:執行緒1 先執行,這時 ready = false,所以進入 else 分支結果為 1
情況2:執行緒2 先執行 num = 2,但沒來得及執行 ready = true,執行緒1 執行,還是進入 else 分支,結 果為1
情況3:執行緒2 執行到 ready = true,執行緒1 執行,這回進入 if 分支,結果為 4(因為 num 已經執行過 了)
但我告訴你,結果還有可能是 0 ,信不信吧!
這種情況下是:執行緒2執行了ready=true,切換到執行緒1,進入if分支,相加為0,再切換回執行緒2執行num=2,相信很多人已經暈了
這種現象叫做指令重排,是 JIT 編譯器在運行時的一些優化,這個現象需要通過大量測驗才能復現:
借助 java 并發壓測工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress
mvn archetype:generate -DinteractiveMode=false
DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-testarchetype -DgroupId=org.sample -DartifactId=test -Dversion=1.0
創建 maven 專案,提供如下測驗類
@JCStressTest
//如果結果是1和4,那么分類到可接受集中,表示結果意料之中
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
//如果結果是0,歸類到感興趣的結果類
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State public
class ConcurrencyTest {
int num = 0;
boolean ready = false;
//這兩個方法將來要通過不同的執行緒來測驗并發,因此需要加上actor標簽
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
執行
mvn clean install
java -jar target/jcstress.jar
會輸出我們感興趣的結果,摘錄其中一次結果:

4.2.解決辦法
volatile 修飾的變數,可以禁用指令重排,然后一個執行緒往volatile變數寫,一個執行緒從volatile變數讀,讀寫操作就不會受到指令重排的影響了,
@JCStressTest
//如果結果是1和4,那么分類到可接受集中,表示結果意料之中
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
//如果結果是0,歸類到感興趣的結果類
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State public
class ConcurrencyTest {
int num = 0;
volatile boolean ready = false;
//這兩個方法將來要通過不同的執行緒來測驗并發,因此需要加上actor標簽
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
結果為:
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.
0 matching test results.
4.3.有序性理解
同一個執行緒內,JVM會在不影響正確性的前提下,可以調整陳述句的執行順序,思考下面一段代碼
static int i;
static int j;
// 在某個執行緒內執行如下賦值操作
i = ...; // 較為耗時的操作
j = ...; //比較快就能得到結果
可以看到,至于是先執行 i 還是 先執行 j ,對終的結果不會產生影響,所以,上面代碼真正執行時, 既可以是
i = ...; // 較為耗時的操作
j = ...; //比較快就能得到結果
也可以是
j = ...; //比較快就能得到結果
i = ...; // 較為耗時的操作
這種特性稱之為『指令重排』,多執行緒下『指令重排』會影響正確性,例如著名的 double-checked locking 模式實作單例
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
//實體沒創建,才會進入內部的 synchronized代碼塊
//多執行緒并發呼叫getinstance方法,有可能會造成單例物件被創建多次
//實際上,我們只需要第一次判斷的時候才需要加鎖,其它時候不需要加鎖,所以需要雙重檢查
if (INSTANCE == null) {
synchronized (Singleton.class) {
// 也許有其它執行緒已經創建實體,所以再判斷一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
以上的實作特點是:
懶惰實體化
首次使用 getInstance() 才使用 synchronized 加鎖,后續使用時無需加鎖
但在多執行緒環境下,上面的代碼是有問題的, INSTANCE = new Singleton() 對應的位元組碼為:
0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field INSTANCE:Lcn/itcast/jvm/t4/Singleton;
第一步,new一個物件,給物件分配空間,它的執行結果會把物件的參考放入運算元堆疊,運算元堆疊把這個物件的參考復制了一份,相當于這時堆疊頂有兩個物件參考,第一個物件參考交給了invokespecial呼叫構造方法,第二個物件參考交給了putstatic給INSTANCE靜態變數賦值,
其中4 7 兩邊的順序不是固定的,也許jvm會優化為:先將參考地址賦值給INSTANCE變數后,再執行構造方法,如果兩個執行緒t1和t2按如下時間序列執行:
時間1 t1 執行緒執行到 INSTANCE = new Singleton();
時間2 t1 執行緒分配空間,為Singleton物件生成了參考地址(0 處)
時間3 t1 執行緒將參考地址賦值給 INSTANCE,這時 INSTANCE != null(7 處)
時間4 t2 執行緒進入getInstance() 方法,發現 INSTANCE != null(synchronized塊外),直接 回傳 INSTANCE
時間5 t1 執行緒執行Singleton的構造方法(4 處)
這時 t1 還未完全將構造方法執行完畢,如果在構造方法中要執行很多初始化操作,那么 t2 拿到的是將 是一個未初始化完畢的單例
對 INSTANCE 使用 volatile 修飾即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才 會真正有效
5. happens-before
happens-before 規定了哪些寫操作對其它執行緒的讀操作可見,它是可見性與有序性的一套規則總結, 拋開以下 happens-before 規則,JMM 并不能保證一個執行緒對共享變數的寫,對于其它執行緒對該共享變 量的讀可見
執行緒解鎖 m 之前對變數的寫,對于接下來對 m 加鎖的其它執行緒對該變數的讀可見
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();
假設上述程式,t1先執行,t2后執行,那么t1修改了之后對t2可見,
執行緒對 volatile 變數的寫,對接下來其它執行緒對該變數的讀可見
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
執行緒start前對變數的寫,對該執行緒開始后對該變數的讀可見
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
執行緒結束前對變數的寫,對其它執行緒得知它結束后的讀可見(比如其它執行緒呼叫 t1.isAlive() 或 t1.join()等待它結束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1"); t1.start();
t1.join();
System.out.println(x);
執行緒 t1 打斷 t2(interrupt)前對變數的寫,對于其他執行緒得知 t2 被打斷后對變數的讀可見(通 過t2.interrupted 或 t2.isInterrupted)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
sleep(1);
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x); }
6.對變數默認值(0,false,null)的寫,對其它執行緒對該變數的讀可見
7.具有傳遞性,如果 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 對 t2 可見, 同時 y=10 也對 t2 可見
System.out.println(x);
},"t2").start();
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/257089.html
標籤:java
上一篇:java執行緒阻塞問題排查
下一篇:二十個集合常見面試題!
