Java happens-before案例

wen java案例 3

Java Happens-Before 关系详解及案例

Happens-Before 是 Java 内存模型(JMM)中最重要的概念之一,它定义了操作之间的内存可见性保证,我将通过多个案例来详细说明。

Java happens-before案例

什么是 Happens-Before?

Happens-Before 关系保证:如果操作 A Happens-Before 操作 B,A 所做的修改对 B 可见,且 A 的执行顺序在 B 之前。

Java 中的 Happens-Before 规则

/**
 * Happens-Before 的 8 种基本规则
 */
public class HappensBeforeRules {
    // 规则1:程序顺序规则
    // 同一个线程中的每个操作都 happens-before 于该线程中后续的任意操作
    // 规则2:监视器锁规则
    // 对一个锁的解锁 happens-before 于随后对这个锁的加锁
    // 规则3:volatile变量规则
    // 对一个 volatile 域的写 happens-before 于任意后续对这个 volatile 域的读
    // 规则4:传递性
    // A happens-before B,且 B happens-before C,A happens-before C
    // 规则5:线程启动规则
    // Thread.start() happens-before 于该线程中的任意操作
    // 规则6:线程终止规则
    // 线程中的所有操作 happens-before 于其他线程检测到该线程终止
    // 规则7:中断规则
    // 线程的 interrupt() 调用 happens-before 于被中断线程检测到中断
    // 规则8:对象终结规则
    // 对象的构造函数结束 happens-before 于 finalize() 方法开始
}

具体案例演示

案例1:volatile 变量的 Happens-Before

public class VolatileExample {
    private volatile boolean flag = false;
    private int value = 0;
    /**
     * 问题:这个示例中,主线程写入 value 和 flag,
     * 读线程能否看到主线程对 value 的修改?
     */
    public void volatileDemo() {
        // 线程A:写入数据
        Thread writer = new Thread(() -> {
            value = 42;          // 1. 普通写
            flag = true;         // 2. volatile写
        });
        // 线程B:读取数据
        Thread reader = new Thread(() -> {
            while (!flag) {      // 3. volatile读
                // 等待
            }
            System.out.println("value = " + value); // 4. 普通读
        });
        writer.start();
        reader.start();
        /**
         * Happens-Before 分析:
         * - 操作1 happens-before 操作2(程序顺序规则)
         * - 操作2 happens-before 操作3(volatile规则)
         * - 操作3 happens-before 操作4(程序顺序规则)
         * - 通过传递性,操作1 happens-before 操作4
         * - 所以线程B一定能看到 value = 42
         */
    }
}

案例2:synchronized 的 Happens-Before

public class SynchronizedExample {
    private int count = 0;
    private final Object lock = new Object();
    /**
     * synchronized 通过锁的获取和释放建立 Happens-Before 关系
     */
    public void synchronizedDemo() {
        // 线程A:写入数据
        new Thread(() -> {
            synchronized (lock) {
                count = 100;    // 1. 在锁内写入
            }                   // 2. 释放锁
        }).start();
        // 线程B:读取数据
        new Thread(() -> {
            synchronized (lock) {   // 3. 获取锁
                // 4. 读取数据,能够看到线程A的修改
                System.out.println("count = " + count);
            }
        }).start();
        /**
         * Happens-Before 分析:
         * - 操作1 happens-before 操作2(程序顺序规则)
         * - 操作2 happens-before 操作3(监视器锁规则:解锁happens-before加锁)
         * - 操作3 happens-before 操作4(程序顺序规则)
         * - 传递性:操作1 happens-before 操作4
         */
    }
}

案例3:线程启动的 Happens-Before

public class ThreadStartExample {
    private int data = 0;
    private boolean initialized = false;
    public void threadStartDemo() {
        data = 88;              // 1. 主线程写入数据
        initialized = true;     // 2. 设置标志
        // 3. 启动新线程
        Thread worker = new Thread(() -> {
            // 4. 新线程中读取数据
            // 通过线程启动规则,一定能看到主线程对 data 和 initialized 的修改
            System.out.println("data = " + data);
            System.out.println("initialized = " + initialized);
        });
        worker.start();         // 触发 Happens-Before
        /**
         * 分析:
         * - 操作1、2 在 start() 之前
         * - start() happens-before 新线程中的任何操作
         * - 所以操作4一定能看到操作1、2的结果
         */
    }
}

案例4:正确使用 Happens-Before 的示例

public class CorrectUsageExample {
    private volatile boolean running = true;
    private int taskCount = 0;
    /**
     * 实际应用:优雅停机
     */
    public void gracefulShutdownDemo() {
        Thread worker = new Thread(() -> {
            while (running) {   // volatile 读
                // 执行任务
                System.out.println("执行任务 " + (++taskCount));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("线程停止");
        });
        worker.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 主线程设置运行标志
        running = false;        // volatile 写
        /**
         * 通过 volatile 的 Happens-Before 关系,
         * worker 线程能够看到 running = false 的修改
         */
    }
}

案例5:利用传递性构建 Happens-Before

public class TransitivityExample {
    private int a = 0;
    private boolean flag = false;
    /**
     * 使用多个同步机制构建 Happens-Before 链
     */
    public void transitivityDemo() {
        Thread producer = new Thread(() -> {
            synchronized (this) {
                a = 10;             // 1. 写入普通变量
                flag = true;        // 2. 写入标志
            }                       // 3. 释放锁
        });
        Thread consumer = new Thread(() -> {
            synchronized (this) {   // 4. 获取锁
                if (flag) {          // 5. 读取标志
                    int value = a;   // 6. 读取普通变量
                    System.out.println("通过传递性看到 a = " + value);
                }
            }
        });
        producer.start();
        consumer.start();
        /**
         * Happens-Before 链:
         * 1 -> 2 -> 3(程序顺序)
         * 3 -> 4(监视器锁规则)
         * 4 -> 5 -> 6(程序顺序)
         * 传递性:1 happens-before 6
         */
    }
}

破坏 Happens-Before 的错误示例

public class WrongExample {
    private int value = 0;
    private boolean ready = false;
    /**
     * 错误示例:没有正确建立 Happens-Before 关系
     */
    public void wrongDemo() {
        // 线程A:写入数据
        new Thread(() -> {
            value = 42;         // 1. 普通写
            ready = true;       // 2. 普通写(不是volatile)
        }).start();
        // 线程B:读取数据
        new Thread(() -> {
            while (!ready) {    // 3. 可能永远看不到 ready 的变化
                // busy wait
            }
            // 4. 即使看到 ready,也可能看不到 value 的最新值
            System.out.println("value = " + value);
        }).start();
        /**
         * 问题:
         * - 没有 volatile,没有 synchronized
         * - 线程B可能永远看不到 ready = true
         * - 即使看到 ready,也可能看到 value = 0
         * - 这里没有建立任何 Happens-Before 关系
         * 
         * 修正方法:
         * - 将 ready 声明为 volatile
         * - 或使用 synchronized 块
         * - 或使用 AtomicBoolean
         */
    }
    // 正确的修正版本
    public void correctDemo() {
        Thread writer = new Thread(() -> {
            value = 42;
            // 使用 synchronized 确保可见性
            synchronized (this) {
                ready = true;
            }
        });
        Thread reader = new Thread(() -> {
            synchronized (this) {
                while (!ready) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("value = " + value);
        });
        writer.start();
        reader.start();
    }
}

常见应用场景总结

public class HappensBeforeApplications {
    /**
     * 1. 双重检查锁定(DCL)- 需要 volatile
     */
    private volatile static HappensBeforeApplications instance;
    public static HappensBeforeApplications getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (HappensBeforeApplications.class) {
                if (instance == null) {            // 第二次检查
                    instance = new HappensBeforeApplications();
                }
            }
        }
        return instance;
    }
    /**
     * 2. 发布对象
     */
    private final Map<String, String> config = new HashMap<>();
    public void publishConfig() {
        config.put("key", "value");  // 在发布前写入
        // 确保正确发布,比如通过 volatile 引用或 synchronized
    }
    /**
     * 3. 并发集合中的 Happens-Before
     */
    public void concurrentCollections() {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        // put 操作 happens-before 后续的 get 操作
        map.put("count", 100);
        // get 操作能看到 put 的结果
        Integer count = map.get("count");
    }
}

关键要点

  1. Happens-Before 不是时间顺序:它强调的是内存可见性保证,不是物理时间上的先后。

  2. 传递性很重要:可以通过多个 Happens-Before 关系串联起来。

  3. 常见误区

    • 认为所有线程都能看到共享变量的修改
    • 认为只需要加锁就能保证可见性
    • 认为 volatile 能保证原子性
  4. 最佳实践

    • 优先使用 java.util.concurrent 包中的工具类
    • 确保正确使用 volatile、synchronized
    • 避免过度优化导致的内存可见性问题

通过理解和使用 Happens-Before 关系,我们可以更好地编写并发程序,避免由内存可见性导致的难以调试的问题。

抱歉,评论功能暂时关闭!