2.Java 内存模型(JMM)

  • 一、JMM 的核心概念
    • JMM(Java Memory Model,Java 内存模型) 是 Java 虚拟机(JVM)规范中定义的一种抽象模型,用于描述 多线程 环境下, 线程如何与主内存(Main Memory)以及工作内存(Working Memory)交互 ,以及如何保证内存的 可见性有序性原子性
    • 1. 主内存与工作内存
      • 主内存(Main Memory)
        • 所有线程 共享 的内存区域,存储变量(实例字段、静态字段等)。
      • 工作内存(Working Memory)
        • 每个线程 私有 的内存区域,存储线程使用的变量的副本。
        • 线程对变量的操作(读取、赋值等)都在工作内存中进行。
    • 2. 内存交互操作
      • (1)lock(锁定) :作用于主内存变量,标识为线程独占。
      • (2)unlock(解锁) :作用于主内存变量,释放锁定状态。
      • (3)read(读取) :从主内存读取变量到工作内存。
      • (4)load(载入) :将 read 操作读取的值放入工作内存的变量副本。
      • (5)use(使用) :将工作内存中的变量值传递给执行引擎。
      • (6)assign(赋值) :将执行引擎计算的结果赋值给工作内存中的变量。
      • (7)store(存储) :将工作内存中的变量值传递到主内存。
      • (8)write(写入) :将 store 操作传递的值写入主内存变量。
    • 3. 内存交互规则
      • (1)顺序性 :read/load 和 store/write 必须成对出现,且顺序不能打乱。
      • (2)可见性 :变量修改后必须同步到主内存,读取时必须从主内存刷新。
      • (3)原子性 :对基本数据类型的操作是原子的(long 和 double 除外)。
  • 二、JMM 的三大特性
    • 1. 原子性(Atomicity)
      • 一个操作要么全部执行,要么全部不执行。
      • 对于 long 和 double,64 位数据类型的操作可能被拆分为两个 32 位操作,因此需要 volatile 关键字保证原子性。
      • JMM 保证对基本数据类型的操作是原子的(如 int、boolean 等)。
  • 示例1:使用 **synchronized** 保证原子性
class Counter {
    private int count = 0;
    public synchronized void increment() {
        count++;  // 确保 count++ 是原子操作
    }
    public int getCount() {
        return count;
    }
}
  • 示例2:使用 **AtomicInteger** 保证原子性
import java.util.concurrent.atomic.AtomicInteger;
class AtomicExample {
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                count.incrementAndGet();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                count.incrementAndGet();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终 count 值:" + count); // 结果必然是 2000
    }
}
  • AtomicInteger 基于 CAS(Compare-And-Swap) 实现无锁操作,能够保证原子性。但它可能会引发 ABA 问题 :即变量从 A 改为 B,又改回 A,CAS 检查发现值没变,但实际变量经历了状态变化。
    - 解决ABA问题: 1、增加版本号:每次更新变量时,同时更新版本号;2、使用 AtomicStampedReference。
    • 2. 可见性(Visibility)
      • 一个线程对共享变量的修改,其他线程能够立即看到。
      • 实现可见性的方式:
        • **volatile 关键字** :强制将修改的值刷新到主内存,并强制从主内存读取最新值。
        • **synchronized 关键字** :在解锁前将变量同步到主内存。
        • **final 关键字** :确保变量在构造完成后对其他线程可见。
          • 示例:使用 volatile 保证可见性
class SharedData {
    private volatile boolean flag = false;
    public void setFlagTrue() {
        flag = true;  // 线程 A 设置 flag 为 true     
    }
    public boolean isFlagTrue() {
        return flag;  // 线程 B 读取 flag     
    }
}
  • 3. 有序性(Ordering)
    • 程序执行的顺序按照代码的先后顺序执行。
    • JMM 通过 happens-before 规则 保证有序性。
    • 指令重排序
      - 编译器和处理器可能会对指令进行重排序以优化性能。
      - JMM 通过内存屏障(Memory Barrier)禁止某些重排序。
      • 示例:使用 volatile 保证有序性
class FlagChecker {
    private volatile boolean flag = false;

    public void setFlagTrue() {
        flag = true;  // 线程 A 设置 flag     
    }

    public boolean isFlagTrue() {
        return flag;  // 线程 B 读取 flag     
    }
}
  • 三、happens-before 规则
    • happens-before ( 先行发生原则 )是 JMM 的核心规则,用于定义操作之间的可见性和顺序性。以下是常见的 happens-before 规则:
    • 程序顺序规则
      • 一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
    • **volatile** 规则
      • 对一个 volatile 变量的写操作,happens-before 于后续对这个变量的读操作。
    • 锁规则
      • 对一个锁的解锁操作,happens-before 于后续对这个锁的加锁操作。
    • 线程启动规则
      • 线程的 start() 方法 happens-before 于该线程的任何操作。
    • 线程终止规则
      • 线程中的所有操作 happens-before 于其他线程检测到该线程已经终止。
    • 传递性规则
      • 如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
    • 示例
class Example {
    int x = 0;
    volatile boolean v = false;
    public void writer() {
        x = 42; // 1
        v = true; // 2
    }
    public void reader() {
        if (v) { // 3
            System.out.println(x); // 4
        }
    }
}
  • 操作 1 happens-before 操作 2(程序顺序规则)。操作 2 happens-before 操作 3(volatile 变量规则)。根据传递性,操作 1 happens-before 操作 4,因此线程 B 读取 x 时能看到线程 A 写入的值 42。
  • 四、 **volatile** 关键字
    • 1. 作用
      • 保证变量的可见性。
      • 禁止指令重排序。
    • 2. 实现原理
      • 内存屏障(Memory Barrier)
        • 写操作后插入 StoreStore 屏障和 StoreLoad 屏障。
        • 读操作前插入 LoadLoad 屏障和 LoadStore 屏障。
      • 强制刷新主内存
        • 写操作后,强制将工作内存中的值刷新到主内存。
        • 读操作前,强制从主内存读取最新值。
    • 3. 使用场景
      • 状态标志(如 boolean flag)。
      • 单例模式的双重检查锁(Double-Checked Locking)。
      • 示例:双重检查锁定
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 保证对象初始化的原子性 
                }
            }
        }
        return instance;
    }
}
  • 五、 **synchronized** 关键字
    • 1. 作用
      • 保证原子性、可见性和有序性。
      • 通过锁机制实现线程同步。
    • 2. 实现原理
      • 对象头
        • 每个对象都有一个对象头,包含锁信息。
      • Monitor 机制
        • 通过 monitorenter 和 monitorexit 指令实现锁的获取和释放。
    • 3. 使用场景
      • 方法同步。
      • 代码块同步。
  • 六、 **final** 关键字
    • 1. 作用
      • 保证变量的不可变性。
      • 确保变量在构造完成后对其他线程可见。
    • 2. 实现原理
      • 禁止指令重排序。
      • 在构造函数中完成初始化后,才能被其他线程访问。
  • 七、JMM 与硬件内存模型
    • 1. 缓存一致性
      • 现代 CPU 使用多级缓存(L1、L2、L3)来加速内存访问。
      • JMM 通过内存屏障保证缓存一致性。
    • 2. 内存屏障
      • LoadLoad :禁止读操作重排序。
      • StoreStore :禁止写操作重排序。
      • LoadStore :禁止读操作与写操作重排序。
      • StoreLoad :禁止写操作与读操作重排序。
  • 八、JMM 的实践与调优
      1. 避免竞态条件
      • 使用 synchronized 或 volatile 保证线程安全。
      • 使用原子类(如 AtomicInteger)实现无锁编程。
      1. 性能优化
      • 减少锁的粒度。
      • 使用无锁数据结构(如 ConcurrentHashMap)。
      1. 调试工具
      • JConsole :监控线程状态。
      • VisualVM :分析线程堆栈。
      • Arthas :动态诊断线程问题。

更多推荐