Java从诞生之初就内置了对多线程编程的支持,这使得Java在企业级后端开发中占据了统治地位。而这一切的基础正是Java内存模型(Java Memory Model,JMM)。JMM规范了多线程程序中,共享变量的访问规则以及线程间的通信行为,它解决了由于CPU缓存、编译器优化和指令重排序带来的可见性、原子性和有序性问题。本文将从硬件架构出发,深入讲解JMM的核心概念——happens-before原则、volatile语义、synchronized的内存效应,并结合实例分析常见并发陷阱。
参考:https://bgnno.cn/category/guide.html
现代多核处理器系统中,每个CPU核心都有自己的高速缓存(L1/L2/L3),主存(RAM)与CPU之间通过缓存一致性协议(如MESI)进行同步。在没有内存模型约束的情况下,编译器可能会对指令进行重排序以优化性能,CPU也可能会乱序执行。这导致在多线程环境下,一个线程对变量的修改可能对其他线程不可见,或者指令执行顺序出乎意料。Java内存模型正是为了屏蔽不同硬件平台的差异,为程序员提供一致的内存访问语义。
JMM的核心是定义了一组happens-before规则,用于判断两个操作之间是否存在顺序保证。如果A happens-before B,则A操作的结果对B操作可见,且A的执行顺序排在B之前。happens-before规则包括:
程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作happens-before后面的操作。
监视器锁规则:对一个锁的解锁操作happens-before后续对同一锁的加锁操作。
volatile变量规则:对一个volatile变量的写操作happens-before后续对该变量的读操作。
传递性:如果A happens-before B,且B happens-before C,则A happens-before C。
线程启动规则:线程的start()方法happens-before该线程内的任何动作。
线程终止规则:线程内的任何动作happens-before其他线程检测到该线程终止(如join()返回)。
中断规则:对线程的interrupt()调用happens-before被中断线程检测到中断。
理解happens-before是编写正确并发程序的关键。例如,以下代码片段可能产生死循环:
public class VisibilityDemo {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
System.out.println("Thread exit");
}).start();
Thread.sleep(1000);
flag = false; // 主线程修改flag
}
}
由于没有同步机制,JVM可能将while(flag)优化为if(!flag) while(true),导致子线程永远看不到flag的更新。解决方案是将flag声明为volatile,这样写操作会立即刷新到主存,读操作每次都从主存读取,并且禁止重排序。
参考:https://rvxif.cn/category/puerh-tea.html
volatile的语义比synchronized更轻量,但不能保证复合操作的原子性(例如count++)。对于原子性需求,应使用java.util.concurrent.atomic包下的类,如AtomicInteger,或者使用synchronized块。synchronized不仅保证了互斥(同一时刻只有一个线程执行代码块),还保证了代码块内的变量修改对后续加锁线程可见。这是因为在释放锁之前,线程会将本地缓存写回主存;获取锁时,会使本地缓存失效,重新从主存读取。
JMM还规定了final域的内存语义:在构造函数内对一个final域的写入,与随后将该对象引用赋值给另一个引用变量之间,存在happens-before关系。这保证了正确构造的对象中,final域的值对所有线程都是可见的,无需额外同步。
更深层次上,JMM允许编译器在不改变单线程语义的前提下进行重排序,但必须遵循as-if-serial语义。对于多线程程序,开发者依赖happens-before规则来确保正确性。Java并发库中的java.util.concurrent类都是基于JMM构建的,例如ReentrantLock使用了AbstractQueuedSynchronizer,其内部利用了volatile变量和CAS操作。
常见并发问题包括:
数据竞争:多个线程同时访问同一变量,至少有一个是写操作,且没有使用happens-before来排序,导致不可预测的结果。
失效数据:线程读取到过期的变量值,通常由于缺乏可见性保证。
指令重排序导致的诡异问题:例如双重检查锁(Double-Checked Locking)在Java 5之前是有问题的,因为对象的初始化可能重排序导致其他线程看到未完全构造的对象。Java 5之后引入volatile解决了该问题。
参考:https://xrzqr.cn/
为了编写可靠的并发代码,应遵循以下原则:
尽量使用高层并发工具:Executors、ConcurrentHashMap、BlockingQueue等,避免直接操作synchronized和volatile。
不可变对象是最简单的共享方式:声明类为final,所有字段为final,不提供修改方法。
线程封闭:使用ThreadLocal将变量限制在线程内部,避免共享。
正确使用volatile:只用于简单的状态标志,且不依赖其复合操作。
理解发布与逸出:不要在构造函数中将this引用暴露给其他线程,否则可能造成未完全构造对象的逸出。
总之,Java内存模型是Java并发编程的基石,它定义了一套规范,使开发者能够编写出跨平台的线程安全代码。深入理解happens-before、volatile、synchronized的内存效应,是成为高级Java工程师的必经之路。