volatile可以保證線程可見性且提供了一定的有序性, 但是無法保證原子性. 在 JVM 規定 volatile 關鍵字執行的前後必須加上
記憶體屏障
. 而真正的底層實作是 LOCK addl 指令鎖總線
小實驗
測試項目位址:
https://gitee.com/zture/spring-test/tree/master/multithreading/src/test/java/cn/diswares/blog
首先做一個小實驗.
小實驗1
- 在一個類聲明一個成員變量 a
- 線程 A 死循環讀取讀取 a 的值, 如果 a != 0 就列印輸出
- 線程 B 等待 1 秒後将 a 的值修改掉
package cn.diswares.blog;
/**
* @author: ztrue
* @date: 2021/3/8
* @version: 1.0
*/
public class VolatileTests {
private static int a = 0;
// private static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (true) {
if (a != 0) {
System.out.println(a);
break;
}
}
}).start();
Thread.sleep(1000);
a = 1;
}
}
預期結果: 程式在 1 秒後就停止運作了
實際結果: 程式陷入死循環
小實驗2
我們重新寫一個測試代碼, 這次為 a 變量加上 volatile 關鍵字, 其它不變
// private static int a = 0;
private static volatile int a = 0;
執行一下, 觀察日志發現程式已經停止了.
原因
線程在運作的時候, 會将 a 的資料從記憶體中讀取出來, copy 到線程的本地記憶體中去, volatile 關鍵字可以保證對象在不痛線程之間的可見性.
那麼為什麼會有這種現象. 這必要知道 CPU 的工作模式, 及線程程序的基本概念
線程和程序的差別
線程: 配置設定資源的基本機關
程序: 線程執行的基本機關
CPU
CPU 的重要組成
CPU 的組成為:
- ALU: 算術邏輯單元, 進行複雜的數學運算
- Registers: 寄存器, 存儲資料
- PC: 指令寄存器, 存儲執行的指令位址
- CACHE: 緩存, 以多核 CPU 舉例. 在一個 CPU 核心内一般有二級緩存, 在多個 CPU 核心間, 存在三級緩存
CPU 工作流程
一個線程在被 CPU 執行前, 需要經過 公共記憶體 -> L3 cache -> L2 cache -> L1 cache. 然後 CPU 将指令位址放進 PC, data 放進 Registers, 再由 ALU 計算後, 将計算結果 從 Cache -> 公共記憶體傳回.
緩存一緻性
從 CPU 工作流程中可以發現, 如果一個變量在多個 CPU 中都存在緩存(一般在多線程程式設計時才會出現), 那麼就可能存緩存一緻性問題.
作業系統解決緩存一緻性的方式
- 緩存一緻性協定: MESI, 64 位核心可以解決 CPU 間 8 byte(緩存行大小)的緩存一緻性問題
- 鎖總線
系統底層保證有序性
- 記憶體屏障: sfence mfence lfence 等系統原語
- 鎖總線
volatile原理
- Java 層級: volatile i
- Byte Code: ACC_VOLATILE
- JVM 層級: 在 volatile 前後都得加記憶體屏障
- hotspot 實作: LOCK addl 指令, 鎖總線