☰
Java 内存模型入门:volatile、synchronized、happens-before
2026/10/2 12:35:15 网站建设 项目流程

1. 引言

并发编程是 Java 开发者进阶的必经之路,而理解 Java 内存模型(Java Memory Model,JMM)则是掌握并发安全的基石。很多初学者在编写多线程程序时,会遇到「明明改了变量,另一个线程却看不到」「程序运行结果不稳定」等诡异问题,这些往往都与 JMM 的三大特性——可见性、原子性、有序性——有关。

本文将从 JMM 的核心概念出发,依次讲解 volatile 能解决什么、不能解决什么,synchronized 的锁升级机制,以及 happens-before 规则,最后通过实战案例帮助你把这些知识串起来。

2. 从 JMM 说起:可见性、原子性、有序性

Java 内存模型规定了多线程环境下共享变量的读写规则,其核心是解决三个问题:可见性、原子性、有序性。

2.1 可见性(Visibility)

可见性指的是:当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。

在 JMM 中,每个线程都有自己的工作内存(可以理解为 CPU 缓存),线程对变量的读写都发生在工作内存中,再同步回主内存。如果缺少同步机制,一个线程的修改可能不会及时刷新到主内存,其他线程读到的就是旧值。

publicclassVisibilityDemo{privatestaticbooleanflag=false;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadreader=newThread(()->{while(!flag){// 空转等待}System.out.println("flag 变为 true,退出循环");});reader.start();Thread.sleep(100);flag=true;// 主线程修改 flag}}

上面的代码中,reader线程可能永远无法退出循环,因为主线程对flag的修改对reader线程不可见。

2.2 原子性(Atomicity)

原子性指的是:一个操作要么全部执行成功,要么全部不执行,中间不能被其他线程打断。

Java 中,对基本类型的赋值和读取(如int x = 1)是原子的,但i++这种「读-改-写」复合操作并不是原子的,它实际上包含三步:读取 i、计算 i+1、写回 i。在多线程环境下,这会导致结果丢失更新。

publicclassAtomicityDemo{privatestaticintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Thread[]threads=newThread[10];for(inti=0;i<10;i++){threads[i]=newThread(()->{for(intj=0;j<10000;j++){count++;}});threads[i].start();}for(Threadt:threads){t.join();}System.out.println("count = "+count);// 结果通常小于 100000}}

2.3 有序性(Ordering)

有序性指的是:程序执行的顺序按照代码的先后顺序执行。但为了提升性能,编译器和 CPU 可能会对指令进行重排序(reordering),只要重排序后的结果与单线程执行结果一致即可。在多线程环境下,重排序可能导致意想不到的问题。

publicclassOrderingDemo{privatestaticintx=0,y=0;privatestaticinta=0,b=0;publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<100000;i++){x=0;y=0;a=0;b=0;Threadt1=newThread(()->{a=1;x=b;});Threadt2=newThread(()->{b=1;y=a;});t1.start();t2.start();t1.join();t2.join();if(x==0&&y==0){System.out.println("第 "+i+" 次出现重排序");}}}}

3. volatile:能解决什么,不能解决什么

volatile 是 Java 提供的最轻量的同步机制,它主要解决可见性和有序性问题。

3.1 volatile 能解决什么

保证可见性:volatile 变量的写操作会立即刷新到主内存,读操作会从主内存读取,从而保证其他线程能看到最新值。

保证有序性:volatile 通过内存屏障禁止指令重排序,保证 volatile 变量读写前后的指令顺序。

publicclassVolatileDemo{privatestaticvolatilebooleanflag=false;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadreader=newThread(()->{while(!flag){// 空转等待}System.out.println("flag 变为 true,退出循环");});reader.start();Thread.sleep(100);flag=true;// volatile 保证 reader 线程能看到}}

3.2 volatile 不能解决什么

不能保证原子性:volatile 无法解决i++这类复合操作的原子性问题,因为「读-改-写」三步之间仍可能被其他线程打断。

publicclassVolatileNotAtomicDemo{privatestaticvolatileintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Thread[]threads=newThread[10];for(inti=0;i<10;i++){threads[i]=newThread(()->{for(intj=0;j<10000;j++){count++;}});threads[i].start();}for(Threadt:threads){t.join();}System.out.println("count = "+count);// 仍然小于 100000}}

适用场景总结:

  • 适合:状态标志位、单次发布不可变对象、作为「轻量级锁」使用。
  • 不适合:需要保证原子性的复合操作、多个变量之间的约束关系。

4. synchronized:锁升级与原理

synchronized 是 Java 内置的同步机制,既能保证原子性,也能保证可见性和有序性。它的底层实现经历了从重量级锁到轻量级锁的演进,这就是所谓的「锁升级」。

4.1 synchronized 的基本用法

publicclassSynchronizedDemo{privateintcount=0;// 同步方法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 B,B happens-before C,则 A happens-before C。

5.2 实战案例:双重检查锁单例

经典的线程安全单例模式,结合 volatile 和 synchronized:

publicclassSingleton{privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){// 第一次检查synchronized(Singleton.class){if(instance==null){// 第二次检查instance=newSingleton();}}}returninstance;}}

这里 volatile 的作用是防止new Singleton()的指令重排序。new操作实际上包含三步:分配内存、初始化对象、将引用指向内存。如果不加 volatile,编译器可能将后两步重排序,导致其他线程拿到一个未初始化完成的对象。

5.3 实战案例:生产者-消费者模型

利用 volatile 作为状态标志,配合 synchronized 保证临界区安全:

publicclassProducerConsumerDemo{privatestaticvolatilebooleanrunning=true;privatestaticfinalObjectlock=newObject();privatestaticintbuffer=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadproducer=newThread(()->{while(running){synchronized(lock){buffer++;System.out.println("生产,buffer = "+buffer);}try{Thread.sleep(50);}catch(InterruptedExceptione){}}});Threadconsumer=newThread(()->{while(running){synchronized(lock){if(buffer>0){buffer--;System.out.println("消费,buffer = "+buffer);}}try{Thread.sleep(80);}catch(InterruptedExceptione){}}});producer.start();consumer.start();Thread.sleep(1000);running=false;// volatile 保证两个线程都能看到停止信号producer.join();consumer.join();}}

6. 总结

本文从 JMM 的三大特性出发,梳理了并发编程的核心知识:

  • 可见性、原子性、有序性是并发安全的三个维度,理解它们是排查并发问题的前提。
  • volatile解决可见性和有序性,但不解决原子性,适合状态标志等轻量场景。
  • synchronized通过锁升级机制在保证原子性的同时兼顾性能,是临界区保护的默认选择。
  • happens-before规则是判断操作间可见性的理论依据,双重检查锁单例是它的经典应用。

在实际开发中,建议优先使用java.util.concurrent包下的工具类(如AtomicInteger、ConcurrentHashMap),它们封装了底层的并发细节,更不容易出错。理解 JMM 的底层原理,能帮助你在遇到疑难并发问题时快速定位根因。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询