Java笔记——JMM
在多线程编程中共享变量的可见性、操作的原子性以及指令的重排序常常成为导致程序出现诡异Bug的罪魁祸首。而Java之所以能够成为并发编程的首选语言之一很大程度上归功于其强大的Java内存模型Java Memory Model, JMM。JMM不仅屏蔽了底层硬件和操作系统的差异还为开发者提供了一套清晰的内存访问规范让“编写一次到处运行”的并发代码成为可能。本文将深入剖析JMM的设计初衷、核心工作机制详解可见性、原子性、有序性三大并发问题并介绍volatile、synchronized以及CAS等解决方案的底层原理帮助你在并发编程中游刃有余。一、为什么需要Java内存模型现代计算机的CPU与内存之间存在着巨大的速度差异为了弥补这一鸿沟CPU引入了多级缓存L1、L2、L3。每个CPU核心都有自己私有的缓存当多个核心同时访问同一块内存数据时就会出现缓存一致性问题。此外为了提高执行效率编译器或处理器可能会对指令进行重排序。这些硬件层面的优化虽然极大提升了单线程性能却给多线程程序带来了不可预期的结果。不同的硬件架构如x86、ARM对缓存一致性和重排序的支持各不相同而Java作为跨平台语言必须屏蔽这些底层差异为上层提供统一的内存访问模型。Java内存模型正是这样一套抽象规范它定义了线程与主内存之间的交互规则保证了Java程序在不同平台上的行为一致性。二、JMM的核心设计主内存与工作内存JMM将内存划分为两个逻辑区域主内存Main Memory所有线程共享的内存区域存储Java对象的实例、静态变量等数据。工作内存Working Memory每个线程私有的内存区域线程对变量的所有操作读取、赋值都必须在工作内存中进行不能直接读写主内存中的变量。线程之间无法直接访问对方的工作内存变量值的传递必须通过主内存完成。当一个线程修改了自己工作内存中的变量副本后需要将其刷新到主内存其他线程才能从主内存中读取到最新值。这种“工作内存-主内存”的交互机制是理解并发问题的关键。JMM定义了8种原子操作来规范主内存与工作内存的交互lock作用于主内存将变量标识为线程独占状态。unlock作用于主内存释放被lock的变量。read作用于主内存将变量值从主内存传输到工作内存。load作用于工作内存将read操作获取的值放入工作内存的变量副本。use作用于工作内存将工作内存中的变量值传递给执行引擎。assign作用于工作内存将执行引擎返回的值赋给工作内存中的变量。store作用于工作内存将工作内存中的变量值传输到主内存。write作用于主内存将store操作的值写入主内存的变量。这些操作保证了变量在传递过程中的原子性但JMM并未限制虚拟机的实现是否允许某些操作合并因此实际性能优化仍然存在。三、JMM的三大并发问题基于上述内存模型多线程环境下会产生三类典型的并发问题1. 可见性问题定义一个线程对共享变量的修改其他线程无法立即看到。原因每个线程都有自己的工作内存对应CPU缓存线程修改变量时先修改工作内存再择机刷新到主内存。其他线程读取时可能仍从自己的缓存中获取旧值。示例public class VisibilityDemo { private static boolean flag true; public static void main(String[] args) throws InterruptedException { new Thread(() - { while (flag) { // 死循环 } System.out.println(线程退出); }).start(); Thread.sleep(1000); flag false; // 主线程修改flag但子线程可能永远看不到 } }上述代码中子线程可能永远无法退出因为主线程修改的flag值没有被子线程感知到。2. 原子性问题定义一个或多个操作在CPU执行过程中不可被中断的特性。在Java中对基本数据类型的读写操作是原子的long/double除外但自增操作如count并非原子操作。原因count在字节码层面包含读取-修改-写入三步线程可能在任意一步被切换导致最终结果出错。示例public class AtomicityDemo { private static int count 0; public static void main(String[] args) throws InterruptedException { Thread t1 new Thread(() - { for (int i 0; i 10000; i) count; }); Thread t2 new Thread(() - { for (int i 0; i 10000; i) count; }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); // 结果通常小于20000 } }3. 有序性问题定义程序执行的顺序与代码编写的顺序不一致。原因编译器和处理器为了优化性能可能会对指令进行重排序前提是重排序不会改变单线程的执行结果即遵守as-if-serial语义。但在多线程环境下重排序可能导致非预期的结果。示例public class OrderingDemo { private static int a 0, b 0, x 0, y 0; public static void main(String[] args) throws InterruptedException { for (int i 0; i 100000; i) { a 0; b 0; x 0; y 0; Thread t1 new Thread(() - { a 1; x b; }); Thread t2 new Thread(() - { b 1; y a; }); t1.start(); t2.start(); t1.join(); t2.join(); if (x 0 y 0) { System.out.println(发生重排序 i); break; } } } }在单线程视角下a1和xb的顺序无所谓但多线程环境下如果x0 y0说明两个线程中的赋值语句被重排序导致相互读到了初始值。四、JMM的解决方案happens-before原则与同步机制为了解决上述并发问题JMM提供了一套基于happens-before原则的规范并结合volatile、synchronized等同步机制来保证程序的正确性。1. happens-before原则定义若两个操作之间存在happens-before关系则第一个操作的结果对第二个操作可见且第一个操作的执行顺序在第二个操作之前。happens-before是JMM定义的一组偏序关系主要包括程序次序规则在一个线程内按照代码顺序前面的操作happens-before后面的操作。volatile变量规则对一个volatile变量的写操作happens-before后续对该变量的读操作。锁规则对一个锁的解锁操作happens-before后续对同一个锁的加锁操作。传递性若A happens-before B且B happens-before C则A happens-before C。线程启动规则Thread.start() happens-before该线程中的任何操作。线程终止规则线程中的任何操作happens-before其他线程检测到该线程终止如Thread.join()返回。中断规则对线程interrupt()的调用happens-before被中断线程检测到中断事件。对象终结规则对象的构造函数执行结束happens-before其finalize()方法。这些规则让开发者无需关心底层内存屏障的具体实现只需遵循规则就能写出正确的并发程序。2. volatile解决可见性与有序性作用保证可见性对volatile变量的写操作会立即刷新到主内存读操作会直接从主内存中读取。禁止指令重排序编译器会在volatile读写前后插入内存屏障防止其与前后代码重排序。底层实现在x86架构下volatile写操作会在汇编指令前加lock前缀该前缀会锁定缓存行并刷新到主内存同时阻止指令重排序。注意volatile不能保证原子性例如volatile int count; count仍然不是原子操作。适用场景状态标志如boolean开关双重检查锁Double-Checked Locking中的单例对象独立观察如读取配置参数3. synchronized保证原子性、可见性与有序性作用原子性synchronized修饰的代码块或方法在同一时刻只能有一个线程执行保证了代码块的原子性。可见性线程在进入synchronized块时会清空工作内存从主内存重新读取变量退出时会将工作内存中的修改刷新到主内存。有序性synchronized块内部依然可能重排序但由于只有一个线程执行不会产生有序性问题从外部看synchronized块内的代码整体上不会与块外重排序。底层实现synchronized依赖JVM的monitor机制编译后会在同步块前后插入monitorenter和monitorexit指令通过锁的互斥保证原子性。4. CASCompare-And-Swap无锁原子操作定义CAS是一种乐观锁技术它包含三个操作数内存位置V、预期原值A、新值B。只有当V的值等于A时才将V更新为B否则什么都不做。整个操作是原子的。Java实现java.util.concurrent.atomic包中的原子类如AtomicInteger底层使用CAS实现通过Unsafe类的compareAndSwapInt等本地方法调用CPU的CAS指令如x86的cmpxchg。ABA问题如果V的值从A变为B再变回ACAS会误认为没有变化。可通过添加版本号如AtomicStampedReference解决。适用场景轻量级并发场景如计数器、状态标志等避免锁带来的上下文切换开销。五、总结Java内存模型是理解并发编程的基石。它通过定义主内存与工作内存的交互规则以及happens-before原则为开发者屏蔽了底层硬件的复杂性。在实际开发中当需要保证可见性和有序性时可以使用volatile关键字。当需要保证原子性时可以使用synchronized或java.util.concurrent.locks.Lock。对于高并发下的计数器等场景可以使用Atomic系列类基于CAS。掌握JMM不仅能帮你写出线程安全的代码还能让你在面对各种诡异的并发Bug时拥有快速定位问题的能力。希望本文能为你打开并发编程世界的一扇窗让你在Java并发之路上走得更稳、更远。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2453352.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!