在多线程编程中,JMM的happens-before规则通过明确操作间的可见性和顺序性,帮助开发者避免数据竞争和线程安全问题。以下是其核心应用场景及实现原理:
一、volatile关键字的可见性保障
场景:
当需要保证共享变量的修改立即对其他线程可见时,例如状态标志位或配置参数。
原理:
根据volatile变量规则,写操作happens-before后续的读操作。JVM通过插入内存屏障禁止指令重排序,并强制将修改刷新到主内存。
示例:
// 线程A volatile boolean flag = false; public void setFlag() { flag = true; // 写操作建立happens-before关系 } // 线程B public void readFlag() { if (flag) { // 读操作可见线程A的修改 // 执行后续逻辑 } }
扩展应用:
- 单例模式的双重检查锁定:
必须用volatile修饰实例变量,防止指令重排序导致“半初始化对象”被其他线程读取。public 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后续的加锁操作。锁释放时,线程的工作内存会强制刷新到主内存;加锁时,线程从主内存重新读取最新值。
示例:
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时,必须在同一把锁的同步块中操作,确保状态修改的可见性。public 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()前的操作(如共享变量初始化)对新线程可见。public 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()后的逻辑。Thread worker = new Thread(() -> { // 执行耗时任务 }); worker.start(); worker.join(); // 等待worker完成后,主线程继续执行
2. 中断处理
- interrupt()规则:
调用interrupt()的操作happens-before被中断线程检测到中断事件。public 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关系,推导复杂操作间的可见性。
示例:
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规则保证写操作对读操作可见。public class AtomicCounter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.getAndIncrement(); // 内部使用volatile保证可见性 } }
2. CountDownLatch与Semaphore
- 原理:
countDown()操作happens-before await()后的逻辑,确保所有线程完成前置任务。public 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(),防止线程在未收到通知时意外唤醒。synchronized (lock) { while (!condition) { lock.wait(); // 循环检查条件 } }
-
非原子的复合操作:
即使变量是volatile,类似count++
的操作仍需同步,因为其包含读取、修改、写入三个步骤。
总结
happens-before规则是多线程编程的基石,通过volatile、synchronized、线程生命周期管理等机制,为开发者提供了可见性和顺序性的保障。在实际应用中,需结合具体场景选择合适的同步策略,并利用传递性规则推导复杂操作间的可见性,同时避免指令重排序和虚假唤醒等陷阱。掌握这些规则能有效提升代码的健壮性和性能。