恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Java 内存模型入门:volatile、synchronized、happens-before
首页
资讯中心
/
Java 内存模型入门:volatile、synchronized、happens-before
Java 内存模型入门:volatile、synchronized、happens-before
发布时间:2026/10/2 12:35:17
1. 引言并发编程是 Java 开发者进阶的必经之路而理解 Java 内存模型Java Memory ModelJMM则是掌握并发安全的基石。很多初学者在编写多线程程序时会遇到「明明改了变量另一个线程却看不到」「程序运行结果不稳定」等诡异问题这些往往都与 JMM 的三大特性——可见性、原子性、有序性——有关。本文将从 JMM 的核心概念出发依次讲解 volatile 能解决什么、不能解决什么synchronized 的锁升级机制以及 happens-before 规则最后通过实战案例帮助你把这些知识串起来。2. 从 JMM 说起可见性、原子性、有序性Java 内存模型规定了多线程环境下共享变量的读写规则其核心是解决三个问题可见性、原子性、有序性。2.1 可见性Visibility可见性指的是当一个线程修改了共享变量的值其他线程能够立即看到这个修改。在 JMM 中每个线程都有自己的工作内存可以理解为 CPU 缓存线程对变量的读写都发生在工作内存中再同步回主内存。如果缺少同步机制一个线程的修改可能不会及时刷新到主内存其他线程读到的就是旧值。publicclassVisibilityDemo{privatestaticbooleanflagfalse;publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadreadernewThread(()-{while(!flag){// 空转等待}System.out.println(flag 变为 true退出循环);});reader.start();Thread.sleep(100);flagtrue;// 主线程修改 flag}}上面的代码中reader线程可能永远无法退出循环因为主线程对flag的修改对reader线程不可见。2.2 原子性Atomicity原子性指的是一个操作要么全部执行成功要么全部不执行中间不能被其他线程打断。Java 中对基本类型的赋值和读取如int x 1是原子的但i这种「读-改-写」复合操作并不是原子的它实际上包含三步读取 i、计算 i1、写回 i。在多线程环境下这会导致结果丢失更新。publicclassAtomicityDemo{privatestaticintcount0;publicstaticvoidmain(String[]args)throwsInterruptedException{Thread[]threadsnewThread[10];for(inti0;i10;i){threads[i]newThread(()-{for(intj0;j10000;j){count;}});threads[i].start();}for(Threadt:threads){t.join();}System.out.println(count count);// 结果通常小于 100000}}2.3 有序性Ordering有序性指的是程序执行的顺序按照代码的先后顺序执行。但为了提升性能编译器和 CPU 可能会对指令进行重排序reordering只要重排序后的结果与单线程执行结果一致即可。在多线程环境下重排序可能导致意想不到的问题。publicclassOrderingDemo{privatestaticintx0,y0;privatestaticinta0,b0;publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti0;i100000;i){x0;y0;a0;b0;Threadt1newThread(()-{a1;xb;});Threadt2newThread(()-{b1;ya;});t1.start();t2.start();t1.join();t2.join();if(x0y0){System.out.println(第 i 次出现重排序);}}}}3. volatile能解决什么不能解决什么volatile 是 Java 提供的最轻量的同步机制它主要解决可见性和有序性问题。3.1 volatile 能解决什么保证可见性volatile 变量的写操作会立即刷新到主内存读操作会从主内存读取从而保证其他线程能看到最新值。保证有序性volatile 通过内存屏障禁止指令重排序保证 volatile 变量读写前后的指令顺序。publicclassVolatileDemo{privatestaticvolatilebooleanflagfalse;publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadreadernewThread(()-{while(!flag){// 空转等待}System.out.println(flag 变为 true退出循环);});reader.start();Thread.sleep(100);flagtrue;// volatile 保证 reader 线程能看到}}3.2 volatile 不能解决什么不能保证原子性volatile 无法解决i这类复合操作的原子性问题因为「读-改-写」三步之间仍可能被其他线程打断。publicclassVolatileNotAtomicDemo{privatestaticvolatileintcount0;publicstaticvoidmain(String[]args)throwsInterruptedException{Thread[]threadsnewThread[10];for(inti0;i10;i){threads[i]newThread(()-{for(intj0;j10000;j){count;}});threads[i].start();}for(Threadt:threads){t.join();}System.out.println(count count);// 仍然小于 100000}}适用场景总结适合状态标志位、单次发布不可变对象、作为「轻量级锁」使用。不适合需要保证原子性的复合操作、多个变量之间的约束关系。4. synchronized锁升级与原理synchronized 是 Java 内置的同步机制既能保证原子性也能保证可见性和有序性。它的底层实现经历了从重量级锁到轻量级锁的演进这就是所谓的「锁升级」。4.1 synchronized 的基本用法publicclassSynchronizedDemo{privateintcount0;// 同步方法publicsynchronizedvoidincrement(){count;}// 同步代码块publicvoidincrementBlock(){synchronized(this){count;}}}4.2 锁升级过程Java 6 之后synchronized 引入了偏向锁、轻量级锁、重量级锁的升级机制目的是在竞争不激烈时减少锁开销。有其他线程竞争自旋失败/竞争加剧无锁状态偏向锁轻量级锁重量级锁偏向锁只有一个线程访问同步块时锁会偏向该线程减少不必要的 CAS 操作。轻量级锁当有第二个线程竞争时偏向锁升级为轻量级锁通过自旋等待获取锁。重量级锁当自旋超过阈值或竞争激烈时升级为重量级锁依赖操作系统互斥量线程会阻塞。4.3 synchronized 与 volatile 的对比特性volatilesynchronized原子性不保证保证可见性保证保证有序性保证禁止重排序保证性能开销较小较大有锁升级优化适用场景状态标志、单次发布复合操作、临界区5. happens-before 规则与实战案例happens-before 是 JMM 定义的一组规则用来判断两个操作之间是否存在「先行发生」关系。如果 A happens-before B那么 A 的执行结果对 B 可见且 A 的指令顺序在 B 之前。5.1 核心 happens-before 规则程序顺序规则同一个线程中前面的操作 happens-before 后面的操作。监视器锁规则对一个锁的解锁 happens-before 后续对这个锁的加锁。volatile 变量规则对一个 volatile 变量的写 happens-before 后续对这个变量的读。传递性规则如果 A happens-before BB happens-before C则 A happens-before C。5.2 实战案例双重检查锁单例经典的线程安全单例模式结合 volatile 和 synchronizedpublicclassSingleton{privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){// 第一次检查synchronized(Singleton.class){if(instancenull){// 第二次检查instancenewSingleton();}}}returninstance;}}这里 volatile 的作用是防止new Singleton()的指令重排序。new操作实际上包含三步分配内存、初始化对象、将引用指向内存。如果不加 volatile编译器可能将后两步重排序导致其他线程拿到一个未初始化完成的对象。5.3 实战案例生产者-消费者模型利用 volatile 作为状态标志配合 synchronized 保证临界区安全publicclassProducerConsumerDemo{privatestaticvolatilebooleanrunningtrue;privatestaticfinalObjectlocknewObject();privatestaticintbuffer0;publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadproducernewThread(()-{while(running){synchronized(lock){buffer;System.out.println(生产buffer buffer);}try{Thread.sleep(50);}catch(InterruptedExceptione){}}});ThreadconsumernewThread(()-{while(running){synchronized(lock){if(buffer0){buffer--;System.out.println(消费buffer buffer);}}try{Thread.sleep(80);}catch(InterruptedExceptione){}}});producer.start();consumer.start();Thread.sleep(1000);runningfalse;// volatile 保证两个线程都能看到停止信号producer.join();consumer.join();}}6. 总结本文从 JMM 的三大特性出发梳理了并发编程的核心知识可见性、原子性、有序性是并发安全的三个维度理解它们是排查并发问题的前提。volatile解决可见性和有序性但不解决原子性适合状态标志等轻量场景。synchronized通过锁升级机制在保证原子性的同时兼顾性能是临界区保护的默认选择。happens-before规则是判断操作间可见性的理论依据双重检查锁单例是它的经典应用。在实际开发中建议优先使用java.util.concurrent包下的工具类如AtomicInteger、ConcurrentHashMap它们封装了底层的并发细节更不容易出错。理解 JMM 的底层原理能帮助你在遇到疑难并发问题时快速定位根因。