Java内存模型的迷雾——从 happens-before 到 volatile 的可见性保证

简介: Java内存模型是Java并发编程的基石,也是最容易被误解的领域之一。

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

目录
相关文章
|
存储 缓存 文件存储
如何保证分布式文件系统的数据一致性
分布式文件系统需要向上层应用提供透明的客户端缓存,从而缓解网络延时现象,更好地支持客户端性能水平扩展,同时也降低对文件服务器的访问压力。当考虑客户端缓存的时候,由于在客户端上引入了多个本地数据副本(Replica),就相应地需要提供客户端对数据访问的全局数据一致性。
32698 79
如何保证分布式文件系统的数据一致性
|
前端开发 容器
HTML5+CSS3前端入门教程---从0开始通过一个商城实例手把手教你学习PC端和移动端页面开发第8章FlexBox布局(上)
HTML5+CSS3前端入门教程---从0开始通过一个商城实例手把手教你学习PC端和移动端页面开发第8章FlexBox布局
17751 20
|
设计模式 存储 监控
设计模式(C++版)
看懂UML类图和时序图30分钟学会UML类图设计原则单一职责原则定义:单一职责原则,所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。bad case:IPhone类承担了协议管理(Dial、HangUp)、数据传送(Chat)。good case:里式替换原则定义:里氏代换原则(Liskov 
36682 19
设计模式(C++版)
|
存储 编译器 C语言
抽丝剥茧C语言(初阶 下)(下)
抽丝剥茧C语言(初阶 下)
|
机器学习/深度学习 人工智能 自然语言处理
带你简单了解Chatgpt背后的秘密:大语言模型所需要条件(数据算法算力)以及其当前阶段的缺点局限性
带你简单了解Chatgpt背后的秘密:大语言模型所需要条件(数据算法算力)以及其当前阶段的缺点局限性
24758 14
|
机器学习/深度学习 弹性计算 监控
重生之---我测阿里云U1实例(通用算力型)
阿里云产品全线降价的一力作,2023年4月阿里云推出新款通用算力型ECS云服务器Universal实例,该款服务器的真实表现如何?让我先测为敬!
36660 15
重生之---我测阿里云U1实例(通用算力型)
|
SQL 存储 弹性计算
Redis性能高30%,阿里云倚天ECS性能摸底和迁移实践
Redis在倚天ECS环境下与同规格的基于 x86 的 ECS 实例相比,Redis 部署在基于 Yitian 710 的 ECS 上可获得高达 30% 的吞吐量优势。成本方面基于倚天710的G8y实例售价比G7实例低23%,总性价比提高50%;按照相同算法,相对G8a,性价比为1.4倍左右。
|
存储 算法 Java
【分布式技术专题】「分布式技术架构」手把手教你如何开发一个属于自己的限流器RateLimiter功能服务
随着互联网的快速发展,越来越多的应用程序需要处理大量的请求。如果没有限制,这些请求可能会导致应用程序崩溃或变得不可用。因此,限流器是一种非常重要的技术,可以帮助应用程序控制请求的数量和速率,以保持稳定和可靠的运行。
29838 52

热门文章

最新文章

下一篇
开通oss服务