引言

  在多线程编程中,可见性原子性有序性是三个核心问题。Java内存模型(JMM)正是为了解决这些问题而设计的规范。它定义了Java程序在多线程环境下的内存访问规则,是并发编程的基石。本文将带你彻底搞懂JMM的核心概念、内存交互操作以及如何解决并发问题。

为什么要了解Java内存模型?

先看一段看似简单的代码:

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;
        System.out.println("主线程已将flag设为false");
    }
}

预期结果:线程会退出循环,输出"线程结束"

实际结果:线程永远不会退出!因为子线程看不到主线程修改的flag值。

这就是可见性问题。JMM正是为了解决这类问题而存在的。

JMM内存模型架构图

JMM内存模型

JMM的核心概念

主内存 vs 工作内存

内存类型 归属 作用 类比
主内存 所有线程共享 存储所有共享变量 数据库
工作内存 每个线程私有 存储变量的副本 CPU缓存

核心规则

  • 所有变量都存储在主内存中
  • 每个线程有自己的工作内存,保存主内存变量的副本
  • 线程对变量的所有操作都必须在工作内存中进行
  • 线程间无法直接访问对方的工作内存
  • 线程间通信必须通过主内存中转

主内存和工作内存

JMM的三大特性

原子性(Atomicity)

一个或多个操作要么全部执行且不被中断,要么全部不执行。

Java中的原子操作

  • 基本类型的读取和赋值是原子的(除了long和double)
  • 通过synchronizedLockAtomicXXX类保证原子性
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicityDemo {
    private static int count = 0;
    private static AtomicInteger atomicCount = new AtomicInteger(0);
    private static final int LOOP = 10000;
    
    public static void main(String[] args) throws InterruptedException {
        // 非原子操作演示
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < LOOP; j++) {
                    // 非原子操作:读-改-写
                    count++;  
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("count++ 结果:" + count + ",预期:" + (10 * LOOP));
        
        // 原子操作演示
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < LOOP; j++) {
                    // 原子操作
                    atomicCount.incrementAndGet();  
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("atomicCount结果:" + atomicCount.get() + ",预期:" + (10 * LOOP));
    }
}

输出结果

count++ 结果:45623,预期:100000
atomicCount结果:100000,预期:100000

可见性(Visibility)

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

保证可见性的方式

  • volatile关键字
  • synchronizedLock
  • final关键字(初始化后可见)
public class VisibilityDemo {
    // 不使用volatile,可能无法看到修改
    private static boolean flag = true;
    // 使用volatile,保证可见性
    // private static volatile boolean flag = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("工作线程启动,flag=" + flag);
            int count = 0;
            while (flag) {
                count++;
                // 添加打印或sleep可能让线程看到修改(因为上下文切换)
                // 但这不是可靠的方式
            }
            System.out.println("工作线程结束,共执行了" + count + "次循环");
        });
        
        worker.start();
        Thread.sleep(1000);
        System.out.println("主线程修改flag为false");
        flag = false;
        
        worker.join();
        System.out.println("程序结束");
    }
}

运行结果(不使用volatile):

工作线程启动,flag=true
主线程修改flag为false
(程序卡住,工作线程永远不会退出)

运行结果(使用volatile):

工作线程启动,flag=true
主线程修改flag为false
工作线程结束,共执行了xxxxx次循环
程序结束

有序性(Ordering)

程序执行的顺序按照代码的先后顺序执行。

为什么会乱序

  • 编译器优化:重排指令以提高性能
  • CPU乱序执行:为充分利用CPU流水线
  • 内存系统重排:缓存导致写操作延迟可见
public class OrderingDemo {
    private static int a = 0, b = 0;
    private static int 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;      // 操作1
                x = b;      // 操作2
            });
            
            Thread t2 = new Thread(() -> {
                b = 1;      // 操作3
                y = a;      // 操作4
            });
            
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            
            // 如果完全按顺序执行,不会出现 x=0,y=0 的情况
            if (x == 0 && y == 0) {
                System.out.println("第" + (i+1) + "次出现重排序:x=" + x + ", y=" + y);
                break;
            }
        }
    }
}

输出结果

第xxx次出现重排序:x=0, y=0

Happens-Before原则

Happens-Before是JMM的核心,它定义了哪些操作必须对后续操作可见。

八大规则

规则 说明 示例
程序次序规则 一个线程内,写在前面的操作先行发生于后面的操作 代码顺序
管程锁定规则 unlock先行发生于后续的lock synchronized块
volatile规则 volatile写先行发生于后续的读 volatile变量
线程启动规则 start()先行发生于线程内的所有操作 Thread.start()
线程终止规则 线程所有操作先行发生于join()返回 Thread.join()
线程中断规则 interrupt()先行发生于检测到中断 Thread.interrupted()
对象终结规则 对象构造完成先行发生于finalize() finalize()
传递性规则 A先行于B,B先行于C,则A先行于C 链式关系
public class HappensBeforeDemo {
    private int value = 0;
    private volatile boolean ready = false;
    
    public void writer() {
        value = 42;      // 操作1
        ready = true;    // 操作2(volatile写)
    }
    
    public void reader() {
        if (ready) {     // 操作3(volatile读)
            System.out.println(value);  // 操作4
            // 由于volatile规则,操作1对操作4可见
            // 所以value一定是42,不会是0
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        HappensBeforeDemo demo = new HappensBeforeDemo();
        
        Thread writer = new Thread(demo::writer);
        Thread reader = new Thread(demo::reader);
        
        writer.start();
        reader.start();
        
        writer.join();
        reader.join();
    }
}

输出结果

42

volatile关键字详解

volatile的两层语义

  1. 保证可见性:写操作立即刷新到主内存,读操作从主内存读取
  2. 禁止指令重排序:在volatile前后设置内存屏障
public class VolatileDemo {
    // 单例模式的双重检查锁(经典volatile使用场景)
    private static volatile VolatileDemo instance;
    
    private VolatileDemo() {}
    
    public static VolatileDemo getInstance() {
        if (instance == null) {           // 第一次检查(非同步)
            synchronized (VolatileDemo.class) {
                if (instance == null) {   // 第二次检查(同步)
                    instance = new VolatileDemo();  // volatile防止指令重排序
                }
            }
        }
        return instance;
    }
}

volatile vs synchronized

特性 volatile synchronized
保证可见性
保证原子性 否(仅单次读写)
保证有序性 是(禁止重排序)
锁机制 无锁 重量级锁
适用场景 状态标记、单例双重检查 复合操作、代码块

代码Demo合集

Demo1:可见性问题演示

public class VisibilityProblemDemo {
    private static boolean running = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("线程开始");
            while (running) {
                // 忙等待,JVM可能优化为 while(true)
            }
            System.out.println("线程结束");
        });
        
        t.start();
        Thread.sleep(1000);
        running = false;
        System.out.println("主线程设置running=false");
    }
}

输出结果

线程开始
主线程设置running=false
(程序永远不会结束)

解决方案:使用volatile修饰running变量

Demo2:volatile解决可见性

public class VolatileSolutionDemo {
    private static volatile boolean running = true;  // 添加volatile
    
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("线程开始");
            while (running) {
                // 现在可以正常退出
            }
            System.out.println("线程结束");
        });
        
        t.start();
        Thread.sleep(1000);
        running = false;
        System.out.println("主线程设置running=false");
        t.join();
        System.out.println("程序正常结束");
    }
}

输出结果

线程开始
主线程设置running=false
线程结束
程序正常结束

Demo3:synchronized解决原子性问题

public class SynchronizedAtomicityDemo {
    private static int count = 0;
    private static final Object lock = new Object();
    private static final int THREAD_COUNT = 10;
    private static final int LOOP = 10000;
    
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREAD_COUNT];
        
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < LOOP; j++) {
                    synchronized (lock) {
                        count++;  // 现在这个操作是原子的
                    }
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("最终结果:" + count + ",预期:" + (THREAD_COUNT * LOOP));
    }
}

输出结果

最终结果:100000,预期:100000

最佳实践与常见问题

什么情况下必须使用volatile?

  • 状态标志:线程停止标志、初始化完成标志
  • 双重检查锁的单例模式
  • 独立观察结果:多个线程读取同一个变量,只有一个线程修改

volatile不能保证原子性的示例

public class VolatileNotAtomicDemo {
    private static volatile int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    count++;  // 非原子操作,volatile不能保证原子性
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("预期:10000,实际:" + count);
    }
}

输出结果

预期:10000,实际:8654(每次结果不同,小于10000)

JVM参数与调试

# 打印JVM内存模型相关信息
java -XX:+PrintFlagsFinal | grep -i "model"

核心总结

  • JMM定义了Java程序在多线程环境下的内存访问规范
  • 主内存存储共享变量,工作内存存储线程副本
  • 三大特性:原子性、可见性、有序性
  • Happens-Before是判断数据竞争和线程安全的核心依据
  • volatile保证可见性和有序性,但不保证复合操作的原子性
  • synchronized可以同时保证三大特性
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐