Java 内存模型(Java Memory Model, JMM)是 Java 语言规范的一部分,它定义了 Java 程序中各个变量(包括实例字段、静态字段和数组元素)的访问规则,以及这些规则如何与硬件内存模型交互。JMM 是理解 Java 多线程编程中可见性、原子性和有序性的关键。
1. 基本概念
1.1 主内存与工作内存
在 JMM 中,所有变量都存储在主内存(Main Memory)中。每个线程都有自己私有的工作内存(Working Memory),工作内存中保存了该线程使用到的变量的副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接操作主内存中的变量。不同线程之间无法直接访问对方的工作内存,线程间变量值的传递需要通过主内存来完成。
1.2 内存间的交互操作
JMM 定义了以下几种内存之间的交互操作:
- read:从主内存读取变量的值到线程的工作内存。
- load:将从主内存读取的变量值放入工作内存的变量副本中。
- use:将工作内存中的变量值传递给执行引擎。
- assign:将执行引擎接收到的值赋给工作内存中的变量。
- store:将工作内存中的变量值写回主内存。
- write:将 store 操作后的值更新到主内存中的变量。
- lock:作用于主内存的变量,把一个变量标识为一条线程独占的状态。
- unlock:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
2. 可见性
可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。在 JMM 中,通过 volatile
关键字可以实现变量的可见性。
2.1 volatile 关键字
volatile
关键字有两个主要特性:
- 可见性:当一个线程修改了
volatile
变量的值,其他线程可以立即看到这个修改。 - 禁止指令重排序:
volatile
变量的写操作不会被编译器或处理器优化重排序。
public class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true;
}
public void reader() {
if (flag) {
// do something
}
}
}
3. 原子性
原子性是指一个操作是不可分割的,要么全部执行,要么完全不执行。在 JMM 中,大多数基本数据类型的读写操作都是原子的,但某些复合操作(如 i++
)不是原子的。
3.1 原子类
为了实现更复杂的原子操作,Java 提供了 java.util.concurrent.atomic
包中的原子类,如 AtomicInteger
、AtomicLong
等。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
4. 有序性
有序性是指程序执行的顺序按照代码的先后顺序执行。然而,由于编译器优化或处理器优化,可能会导致指令重排序,从而破坏程序的有序性。在 JMM 中,通过 volatile
关键字和 synchronized
关键字可以保证有序性。
4.1 happens-before 原则
JMM 通过 happens-before 原则来保证内存的可见性和有序性。happens-before 原则包括以下几条规则:
- 程序顺序规则:在一个线程内,按照代码顺序,前面的操作 happens-before 后面的操作。
- 监视器锁规则:一个解锁操作 happens-before 后面对同一个锁的加锁操作。
- volatile 变量规则:对一个 volatile 变量的写操作 happens-before 后面对同一个变量的读操作。
- 线程启动规则:线程的启动 happens-before 线程的任何操作。
- 线程终止规则:线程的所有操作 happens-before 线程的终止。
- 中断规则:对线程的中断 happens-before 被中断线程检测到中断。
- 传递性:如果 A happens-before B,且 B happens-before C,则 A happens-before C。
5. 实际生产案例
5.1 单例模式的双重检查锁定
在多线程环境中,单例模式的实现需要特别注意线程安全。双重检查锁定(Double-Check Locking, DCL)是一种常见的实现方式,它利用 volatile
关键字来保证实例的可见性和有序性。
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
5.2 线程安全的计数器
在多线程环境下,普通的计数器可能会因为指令重排序而出现线程安全问题。使用 AtomicInteger
可以确保计数器的线程安全性。
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
5.3 生产者-消费者模型
生产者-消费者模型是典型的多线程协作问题。使用 BlockingQueue
可以简化线程间的通信和同步。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumer {
private final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
public class Producer implements Runnable {
@Override
public void run() {
try {
while (true) {
queue.put(produce());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private Integer produce() {
// 生产数据
return 1;
}
}
public class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
consume(queue.take());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void consume(Integer item) {
// 消费数据
System.out.println("Consumed: " + item);
}
}
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producer = new Thread(pc.new Producer());
Thread consumer = new Thread(pc.new Consumer());
producer.start();
consumer.start();
}
}
总结
Java 内存模型是 Java 多线程编程的基础,理解其核心概念和原则对于编写高效、可靠的并发程序至关重要。通过合理使用 volatile
、synchronized
和并发工具类,可以有效解决多线程环境下的可见性、原子性和有序性问题。希望本文能帮助你更好地掌握 Java 内存模型,提升你的多线程编程能力。