天天看點

Volatile保證可見性的原理-Java多線程(四)小實驗線程和程序的差別CPU緩存一緻性系統底層保證有序性volatile原理

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 指令, 鎖總線