Java内存模型是Java并发编程的基石,也是最容易被误解的领域之一。它定义了多线程程序中共享变量的访问规则,回答了这样一个核心问题:当一个线程修改了共享变量,其他线程何时能看到这个修改?理解Java内存模型,不仅是写出正确并发程序的前提,也是理解volatile、synchronized、final等关键字语义的关键。
参考:https://xbivx.cn/category/travel-advice.html
Java内存模型的核心是happens-before关系。如果两个操作之间存在happens-before关系,那么第一个操作的结果对第二个操作可见,且第一个操作的执行顺序排在第二个操作之前。happens-before规则包括:程序顺序规则(同一个线程中,写在前面的操作happens-before后面的操作)、监视器锁规则(对一个锁的解锁happens-before随后对同一个锁的加锁)、volatile变量规则(对volatile变量的写happens-before随后对同一个volatile变量的读)、线程启动规则(线程的start调用happens-before该线程中的任何操作)、线程终止规则(线程中的任何操作happens-before其他线程检测到该线程终止)等。
volatile关键字是Java中最轻量级的同步机制。它保证了对volatile变量的读写具有可见性——写入volatile变量的值立即对其他线程可见。但volatile不保证原子性,复合操作如count++在volatile变量上仍然不是线程安全的。volatile的底层实现涉及内存屏障——编译器在生成volatile读写指令时插入特殊的屏障,禁止某些重排序,并强制刷新CPU缓存到主内存。
与C++的volatile不同,Java的volatile不用于硬件映射变量,而是专门用于多线程通信。一个常见的误区是认为volatile可以替代锁。实际上,volatile只适用于以下场景:写入变量不依赖于当前值(如设置标志位)、该变量不与其他状态变量共同构成不变量。其他情况都需要使用锁或原子类。
参考:https://xbivx.cn/category/disaster-warning.html
synchronized关键字提供了更强大的同步语义。它保证互斥(同一时间只有一个线程可以执行同步块)和可见性(解锁前对共享变量的修改对后续加锁的线程可见)。synchronized可以用于实例方法、静态方法和代码块。JVM对synchronized进行了大量优化:偏向锁、轻量级锁、重量级锁的自适应升级,以及锁消除和锁粗化等编译器优化。
final字段在Java内存模型中有特殊语义。在构造函数中正确初始化后的final字段,在其他线程中可见时一定处于已初始化状态,无需同步。这为不可变对象的安全发布提供了保证。但注意,如果final字段指向一个可变对象,该对象内部的状态仍然需要额外同步。
双重检查锁定的陷阱是Java并发编程中的经典反模式。在没有volatile修饰的情况下,双重检查锁定可能返回未完全初始化的对象,因为构造函数和引用赋值可能被重排序。Java 5之后,通过将单例变量声明为volatile修复了这个问题,但更好的替代方案是使用静态内部类或枚举单例。
Java内存模型允许编译器、处理器和运行时对指令进行重排序,只要不改变单线程程序的语义。这种重排序是性能优化的关键,但在多线程环境下可能导致违反直觉的结果。例如,在没有同步的情况下,一个线程写入两个变量的顺序,在其他线程看来可能被颠倒。happens-before规则就是用来禁止这种危险重排序的。
现代CPU的缓存架构加剧了可见性问题。每个CPU核心都有自己的L1、L2缓存,对共享变量的修改可能停留在缓存中,未刷新到主内存。Java内存模型要求JVM在必要时插入内存屏障,强制缓存一致性协议(如MESI)进行同步。但内存屏障是有成本的,过度使用会损害性能。
参考:https://xbivx.cn/category/weather-knowledge.html
发布与逸出是另一个重要概念。对象的安全发布意味着其他线程看到的对象处于完全构造且一致的状态。不安全的发布可能导致其他线程看到部分构造的对象(如未初始化的字段)。安全发布的方法包括:在静态初始化器中初始化对象引用、将引用存储在volatile字段或AtomicReference中、将引用存储在正确构造的对象的final字段中、或者使用锁来保护访问。
Java内存模型的理论基础来自顺序一致性。顺序一致性是一种理想的内存模型,其中所有操作按照程序顺序执行,且所有线程看到的操作顺序相同。Java内存模型允许比顺序一致性更多的重排序,以获得更好的性能,但通过happens-before规则限制了重排序的范围,确保正确同步的程序表现出顺序一致的行为。
对于大多数开发者来说,不需要深入理解Java内存模型的每一个细节。遵循以下原则就足以写出正确的并发程序:尽可能使用java.util.concurrent包中的高级并发工具;使用volatile只做标志位;使用synchronized或Lock保护共享变量的所有访问;尽量使用不可变对象;以及通过JCStress等工具进行并发测试。
参考:https://xbivx.cn