happens-before 关系

2、happens-before 关系

在 Java 中,volatile 关键字用于变量的修饰,它确保对该变量的所有读写操作都是直接从主内存中进行的,而不是从线程的本地缓存

中读取。volatile 关键字可以保证某些类型的内存可见性,并在一定程度上防止指令重排序。具体来说,volatile 可以建立一种特殊

的 happens-before 关系,确保多线程程序的正确性和一致性。

happens-before 关系是 Java 内存模型(JMM)中的一种重要概念,用于定义线程之间操作的顺序性。简单来说,如果操作 A happens-

before 操作 B ,那么操作 A 的结果对操作 B 是可见的,并且操作 A 的顺序在操作 B 之前。

对于 volatile 变量,有以下几种 happens-before 关系:

  1. volatile 变量的写操作 happens-before 随后的读操作
    • 如果线程 A 对一个 volatile 变量进行写操作,然后线程 B 对这个 volatile 变量进行读操作,那么在 A 线程中对这volatile 变量的写操作 happens-before B 线程中的读操作。这意味着线程 B 将看到线程 A 写入的最新值。
  2. volatile 变量的写操作会禁止写之前的所有操作被重排序到写操作之后
    • 在对 volatile 变量进行写操作之前的所有操作,在内存模型上会被"刷回"主内存。即对 volatile 变量的写操作之前的所有普通变量的操作都将在写操作之前完成,并且在写操作之前的所有操作对后续的任何线程都是可见的。
  3. volatile 变量的读操作会禁止读之后的所有操作被重排序到读操作之前
    • 在对 volatile 变量进行读操作之后的所有操作,在内存模型上会从主内存读取最新值。即对 volatile 变量的读操作之后的所有普通变量的操作都将在读操作之后完成,并且在读操作之后的所有操作将看到写操作之后的最新结果。
示例代码

以下是一个简单的代码示例,展示了 volatile 的 happens-before 关系:

java 复制代码
public class VolatileHappensBeforeExample {
//    private volatile boolean flag = false;
    private boolean flag = false;
    private int counter = 0;

    public void writer() {
        counter = 1;        // 普通写操作
        flag = true;        // volatile 写操作
    }

    public void reader() {
        if (flag) {         // volatile 读操作
            System.out.println(counter);  // 普通读操作
        }
    }

    public static void main(String[] args) {
        VolatileHappensBeforeExample example = new VolatileHappensBeforeExample();

        for (int i = 0; i < 10; i++) {
            // 创建写线程
            Thread writerThread = new Thread(() -> {
                example.writer();
            });

            // 创建读线程
            Thread readerThread = new Thread(() -> {
                example.reader();
            });

            writerThread.start();
            readerThread.start();
        }

    }
}

在这个示例中:

  • writer() 方法中,对 flag 的写操作 happens-before 随后的 reader() 方法中对 flag 的读操作。
  • 因此,如果 reader() 方法检测到 flagtrue,则它必然会看到 counter 的值为 1(即 writer() 方法中的写操作已发生)。

这种 happens-before 关系确保了多线程环境中的变量更新对于其他线程是可见的,从而保证了线程之间的正确通信。

上面这么啰里巴嗦地讲,这也太抽象了,即使去掉 volatile 修饰其实也不一定会出现打印不出来1的情况,必须整个程序验证一下。

验证 happens-before 关系

验证不使用 volatile 关键字会导致错误,可以通过编写一个多线程测试程序,观察在不同线程之间的共享变量是否会出现不可见性问题。具体来说,可以通过运行代码并检测在某些情况下是否会出现预期之外的结果,例如永远不会打印出预期的值

要验证不使用 volatile 关键字会导致错误,可以通过编写一个多线程测试程序,观察在不同线程之间的共享变量是否会出现不可见性问题。具体来说,可以通过运行代码并检测在某些情况下是否会出现预期之外的结果,例如永远不会打印出预期的值。

验证代码

以下是一个示例代码,通过多个线程的交互来验证如果不使用 volatile 关键字会出现的问题:

java 复制代码
public class VolatileHappensBeforeExample {
//    private boolean flag = false;  // 没有使用 volatile
    private volatile boolean flag = false;  // 使用 volatile
    private int counter = 0;

    public void writer() {
        counter = 1;        // 普通写操作
        flag = true;        // 普通写操作
        System.out.println(Thread.currentThread().getName() + " set flag to true");
    }

    public void reader() {
        while (!flag) {
            // Busy-wait loop, waiting for flag to become true
        }
        System.out.println(Thread.currentThread().getName() + " sees flag is true and counter is " + counter);
    }

    public static void main(String[] args) {
        VolatileHappensBeforeExample example = new VolatileHappensBeforeExample();

        // 创建写线程
        Thread writerThread = new Thread(() -> {
            try {
                Thread.sleep(100);  // 确保 reader 线程先启动
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            example.writer();
        }, "WriterThread");

        // 创建读线程
        Thread readerThread = new Thread(() -> {
            example.reader();
        }, "ReaderThread");

        readerThread.start();
        writerThread.start();

        try {
            readerThread.join();
            writerThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

代码解释

  1. writer() 方法
    • 设置 counter 为 1。
    • 设置 flag 为 true。
    • 打印当前线程名称及其操作。
  2. reader() 方法
    • 使用一个 busy-wait 循环等待 flag 变为 true。
    • flag 为 true 时,打印当前线程名称及其看到的 counter 值。
  3. main 方法
    • 创建并启动 writerThreadreaderThread
    • 使用 Thread.sleep(100) 确保 readerThread 先启动。

可能的结果

运行上述代码多次,可能会看到以下结果:

  • 有时,程序会如预期输出 WriterThread set flag to trueReaderThread sees flag is true and counter is 1
  • 但在某些运行中,可能会看到 WriterThread set flag to true,但 readerThread 进入 busy-wait 循环后永远不会退出。这是因为 readerThread 可能无法看到 flag 被设置为 true 的更新。
结论

如果不使用 volatile 关键字,flag 的写入更新对其他线程不可见,导致 readerThread 无法检测到 flag 的变化并一直在 busy-wait 循环中。这验证了不使用 volatile 关键字时可能出现的内存可见性问题。

通过多次运行这个程序,观察到 readerThread 不会始终成功读取到 flag 的变化,就可以确认不使用 volatile 关键字会导致多线程程序中的错误。这种错误在 volatile 关键字存在时不会发生,因为 volatile 能确保内存可见性和建立正确的 happens-before 关系。

相关推荐
星河梦瑾44 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富1 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想1 小时前
JMeter 使用详解
java·jmeter
言、雲1 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇1 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
Yvemil71 小时前
《开启微服务之旅:Spring Boot 从入门到实践》(三)
java
Anna。。1 小时前
Java入门2-idea 第五章:IO流(java.io包中)
java·开发语言·intellij-idea
.生产的驴2 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
爱上语文2 小时前
宠物管理系统:Dao层
java·开发语言·宠物
王ASC2 小时前
SpringMVC的URL组成,以及URI中对/斜杠的处理,解决IllegalStateException: Ambiguous mapping
java·mvc·springboot·web