在多线程编程中,JMM的happens-before规则通过明确操作间的可见性和顺序性,帮助开发者避免数据竞争和线程安全问题。以下是其核心应用场景及实现原理:
一、volatile关键字的可见性保障
场景 :
当需要保证共享变量的修改立即对其他线程可见时,例如状态标志位或配置参数。
原理 :
根据volatile变量规则,写操作happens-before后续的读操作。JVM通过插入内存屏障禁止指令重排序,并强制将修改刷新到主内存。
示例:
java
// 线程A
volatile boolean flag = false;
public void setFlag() {
flag = true; // 写操作建立happens-before关系
}
// 线程B
public void readFlag() {
if (flag) { // 读操作可见线程A的修改
// 执行后续逻辑
}
}
扩展应用:
-
单例模式的双重检查锁定 :
必须用volatile修饰实例变量,防止指令重排序导致"半初始化对象"被其他线程读取。javapublic class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // 禁止指令重排序 } } } return instance; } }
二、synchronized的锁可见性
场景 :
需要保证复合操作(如"检查-修改-写入")的原子性和可见性,例如计数器或共享资源访问。
原理 :
根据锁定规则,解锁操作happens-before后续的加锁操作。锁释放时,线程的工作内存会强制刷新到主内存;加锁时,线程从主内存重新读取最新值。
示例:
java
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++; // 写操作对后续加锁线程可见
}
}
public int getCount() {
synchronized (lock) {
return count; // 读操作获取最新值
}
}
}
扩展应用:
-
线程间协作 :
使用wait/notify时,必须在同一把锁的同步块中操作,确保状态修改的可见性。javapublic class ProducerConsumer { private final Object lock = new Object(); private int data = 0; private boolean hasData = false; public void produce() { synchronized (lock) { while (hasData) { lock.wait(); // 释放锁并等待 } data = 42; hasData = true; lock.notifyAll(); // 唤醒消费者 } } public int consume() { synchronized (lock) { while (!hasData) { lock.wait(); // 等待数据可用 } hasData = false; lock.notifyAll(); // 唤醒生产者 return data; } } }
三、线程间协作与生命周期管理
1. 线程启动与终止
-
start()规则 :
主线程调用start()前的操作(如共享变量初始化)对新线程可见。
javapublic class WorkerThread extends Thread { private volatile int sharedData; public void setSharedData(int value) { sharedData = value; // start()前的写操作对线程可见 } @Override public void run() { // 可安全读取sharedData的值 } }
-
join()规则 :
子线程的所有操作happens-before主线程调用join()后的逻辑。
javaThread worker = new Thread(() -> { // 执行耗时任务 }); worker.start(); worker.join(); // 等待worker完成后,主线程继续执行
2. 中断处理
-
interrupt()规则 :
调用interrupt()的操作happens-before被中断线程检测到中断事件。javapublic class InterruptibleTask implements Runnable { private volatile boolean running = true; @Override public void run() { while (running && !Thread.currentThread().isInterrupted()) { // 执行任务 } } public void stop() { running = false; // 配合interrupt()确保可见性 Thread.currentThread().interrupt(); } }
四、传递性规则的复合场景
场景 :
通过组合多个happens-before关系,推导复杂操作间的可见性。
示例:
java
public class TransitiveExample {
private int a = 0;
private volatile boolean flag1 = false;
private volatile boolean flag2 = false;
public void writeA() {
a = 1; // 操作1
flag1 = true; // 操作2(程序顺序规则:1 happens-before 2)
}
public void writeFlag2() {
if (flag1) { // 操作3(volatile规则:2 happens-before 3)
flag2 = true; // 操作4(程序顺序规则:3 happens-before 4)
}
}
public void readA() {
if (flag2) { // 操作5(volatile规则:4 happens-before 5)
// 通过传递性,操作1的结果对操作5可见,a必定为1
}
}
}
五、原子类与并发工具的底层依赖
1. Atomic类的可见性
-
原理 :
原子类(如AtomicInteger)通过volatile和CAS(Compare-And-Swap)操作实现可见性,利用volatile规则保证写操作对读操作可见。javapublic class AtomicCounter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.getAndIncrement(); // 内部使用volatile保证可见性 } }
2. CountDownLatch与Semaphore
-
原理 :
countDown()操作happens-before await()后的逻辑,确保所有线程完成前置任务。javapublic class TaskManager { private CountDownLatch latch = new CountDownLatch(3); public void executeTasks() { for (int i = 0; i < 3; i++) { new Thread(() -> { // 执行任务 latch.countDown(); // 任务完成通知 }).start(); } try { latch.await(); // 等待所有任务完成 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
六、避免常见错误
-
未正确使用volatile的双重检查锁定 :
若instance未用volatile修饰,JVM可能重排序导致其他线程读取到未初始化的对象。
-
wait/notify的虚假唤醒 :
必须在循环中调用wait(),防止线程在未收到通知时意外唤醒。
javasynchronized (lock) { while (!condition) { lock.wait(); // 循环检查条件 } }
-
非原子的复合操作 :
即使变量是volatile,类似
count++
的操作仍需同步,因为其包含读取、修改、写入三个步骤。
总结
happens-before规则是多线程编程的基石,通过volatile 、synchronized 、线程生命周期管理等机制,为开发者提供了可见性和顺序性的保障。在实际应用中,需结合具体场景选择合适的同步策略,并利用传递性规则推导复杂操作间的可见性,同时避免指令重排序和虚假唤醒等陷阱。掌握这些规则能有效提升代码的健壮性和性能。