Java之併發三問題
轉載自:https://javadoop.com/post/java-memory-model#toc10
1. 重排序
請先執行下面的程式碼
public class Test {
private static int x = 0, y = 0;
private static int a = 0, b =0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) {
}
a = 1;
x = b;
});
Thread other = new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) {
}
b = 1;
y = a;
});
one.start();other.start();
latch.countDown();
one.join();other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
幾秒後,我們就可以得到 x == 0 && y == 0 這個結果,仔細看看程式碼就會知道,如果不發生重排序的話,這個結果是不可能出現的。
重排序由以下幾種機制引起:
編譯器優化:對於沒有資料依賴關係的操作,編譯器在編譯的過程中會進行一定程度的重排。
大家仔細看看執行緒 1 中的程式碼,編譯器是可以將 a = 1 和 x = b 換一下順序的,因為它們之間沒有資料依賴關係,同理,執行緒 2 也一樣,那就不難得到 x == y == 0 這種結果了。
指令重排序:CPU 優化行為,也是會對不存在資料依賴關係的指令進行一定程度的重排。
這個和編譯器優化差不多,就算編譯器不發生重排,CPU 也可以對指令進行重排,這個就不用多說了。
記憶體系統重排序:記憶體系統沒有重排序,但是由於有快取的存在,使得程式整體上會表現出亂序的行為。
假設不發生編譯器重排和指令重排,執行緒 1 修改了 a 的值,但是修改以後,a 的值可能還沒有寫回到主存中,那麼執行緒 2 得到 a == 0 就是很自然的事了。同理,執行緒 2 對於 b 的賦值操作也可能沒有及時重新整理到主存中。
2. 記憶體可見性
前面在說重排序的時候,也說到了記憶體可見性的問題,這裡再囉嗦一下。
執行緒間的對於共享變數的可見性問題不是直接由多核引起的,而是由多快取引起的。如果每個核心共享同一個快取,那麼也就不存在記憶體可見性問題了。
現代多核 CPU 中每個核心擁有自己的一級快取或一級快取加上二級快取等,問題就發生在每個核心的獨佔快取上。每個核心都會將自己需要的資料讀到獨佔快取中,資料修改後也是寫入到快取中,然後等待刷入到主存中。所以會導致有些核心讀取的值是一個過期的值。
Java 作為高階語言,遮蔽了這些底層細節,用 JMM 定義了一套讀寫記憶體資料的規範,雖然我們不再需要關心一級快取和二級快取的問題,但是,JMM 抽象了主記憶體和本地記憶體的概念。
所有的共享變數存在於主記憶體中,每個執行緒有自己的本地記憶體,執行緒讀寫共享資料也是通過本地記憶體交換的,所以可見性問題依然是存在的。這裡說的本地記憶體並不是真的是一塊給每個執行緒分配的記憶體,而是 JMM 的一個抽象,是對於暫存器、一級快取、二級快取等的抽象。
3. 原子性
在本文中,原子性不是重點,它將作為併發程式設計中需要考慮的一部分進行介紹。
說到原子性的時候,大家應該都能想到 long 和 double,它們的值需要佔用 64 位的記憶體空間,Java 程式語言規範中提到,對於 64 位的值的寫入,可以分為兩個 32 位的操作進行寫入。本來一個整體的賦值操作,被拆分為低 32 位賦值和高 32 位賦值兩個操作,中間如果發生了其他執行緒對於這個值的讀操作,必然就會讀到一個奇怪的值。
這個時候我們要使用 volatile 關鍵字進行控制了,JMM 規定了對於 volatile long 和 volatile double,JVM 需要保證寫入操作的原子性。
另外,對於引用的讀寫操作始終是原子的,不管是 32 位的機器還是 64 位的機器。
Java 程式語言規範同樣提到,鼓勵 JVM 的開發者能保證 64 位值操作的原子性,也鼓勵使用者儘量使用 volatile 或使用正確的同步方式。關鍵詞是”鼓勵“。
相關文章
- Java併發指南3:併發三大問題與volatile關鍵字,CAS操作Java
- Java併發之ReentrantLock原始碼解析(三)JavaReentrantLock原始碼
- Java併發之ThreadPoolExecutor原始碼解析(三)Javathread原始碼
- 併發程式設計之 Java 三把鎖程式設計Java
- 併發問題的三大根源是什麼?
- java併發之ConcurrentLinkedQueueJava
- java併發之synchronizedJavasynchronized
- Java多執行緒和併發問題集Java執行緒
- 聊聊Java併發面試問題之公平鎖與非公平鎖是啥?Java面試
- java併發程式設計 --併發問題的根源及主要解決方法Java程式設計
- Java併發系列之volatileJava
- Java併發包之 CopyOnWriteArrayListJava
- Java併發面試題Java面試題
- (三)Java高併發秒殺系統API之Web層開發JavaAPIWeb
- Java併發專題(三)深入理解volatile關鍵字Java
- 「分散式技術專題」併發系列三:樂觀併發控制之原型系統分散式原型
- 【搞定 Java 併發面試】面試最常問的 Java 併發基礎常見面試題總結!Java面試題
- java併發之hashmap原始碼JavaHashMap原始碼
- Java 併發包之CountDownLatch、CyclicBarrierJavaCountDownLatch
- Java併發之AQS詳解JavaAQS
- Java併發之AQS原理剖析JavaAQS
- Java併發之CompletionService詳解Java
- Java併發之顯式鎖Java
- 「分散式技術專題」併發系列三:樂觀併發控制之生產系統分散式
- 併發問題處理方式
- 併發背後的問題
- Java 併發面試題解Java面試題
- 【高併發】學好併發程式設計,關鍵是要理解這三個核心問題程式設計
- 啃碎併發(五):Java執行緒安全特性與問題Java執行緒
- Java工作中的併發問題處理方法總結Java
- SpringBoot實現Java高併發秒殺系統之Web層開發(三)Spring BootJavaWeb
- Java併發程式設計之Java CAS操作Java程式設計
- java併發之SynchronousQueue實現原理Java
- Java併發程式設計之synchronizedJava程式設計synchronized
- Java併發之等待/通知機制Java
- Java高併發之CyclicBarrier簡介Java
- java併發之volatile關鍵字Java
- Java併發程式設計之執行緒篇之執行緒中斷(三)Java程式設計執行緒