什么是Happens-Before?

什么是 JavaHappens-Before?

happens-before 是 Java 内存模型中最重要的概念之一。它定义了一种偏序关系,用来描述一个操作 A 是否可以"看到"另一个操作 B 的执行结果。

2024最全大厂面试题无需C币点我下载或者在网页打开全套面试题已打包

AI绘画关于SD,MJ,GPT,SDXL,Comfyui百科全书

准确地说,如果一个操作 A happens-before 于另一个操作 B,那么 A 执行的结果对 B 来说就是可见的。换句话说,B 就可以看到 A 的执行结果。反之,如果 A 不 happens-before B,那么 B 能否看到 A 的结果就是不确定的。

这个概念可能看起来很抽象,不过一旦理解了它的本质,你就会发现它无处不在,贯穿于 Java 并发编程的方方面面。接下来,让我们通过几个生动的例子来感受一下 happens-before 在实际应用中的威力。

示例 1: 单线程中的 happens-before

在单线程环境中,happens-before 规则非常简单:

  1. 程序顺序规则: 在一个线程中,按照代码的顺序执行,前面的操作happens-before后面的操作。

下面的代码片段就是一个典型的例子:

int a = 1;
int b = 2;

根据程序顺序规则,a = 1 happens-before b = 2。也就是说,在这个线程中,b = 2 一定能看到 a = 1 的结果。

  1. volatile 变量规则: 对一个 volatile 变量的写操作,happens-before于后面对这个变量的读操作。

假设我们有一个 volatile 变量 flag:

volatile boolean flag = false;
// ...
flag = true;
// ...
if (flag) {
    // do something
}

在这里,flag = true happens-before if (flag)。也就是说,当执行 if (flag) 时,一定能看到 flag = true 的结果。

  1. 监视器规则: 一个线程中,unlock 一个监视器happens-before于后面对这个监视器的 lock 操作。
synchronized (obj) {
    // ...
}

在这个代码块中,退出 synchronized 块 (unlock) happens-before于后续再次进入 synchronized 块 (lock)。

总的来说,在单线程环境下,happens-before 规则是非常直观和容易理解的。但是,当涉及到多线程环境时,情况就变得复杂多了。

示例 2: 多线程中的 happens-before

在多线程环境下,happens-before 规则就变得更加复杂和重要了。我们来看一个例子:

public class HappensBeforeExample {
    private static int x = 0, y = 0;
    private static volatile int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            a = 1;
            x = b;
        });
        Thread t2 = new Thread(() -> {
            b = 1;
            y = a;
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("x = " + x + ", y = " + y);
    }
}

在这个例子中,我们有两个线程 t1t2。每个线程都会修改一个 volatile 变量,然后读取另一个线程修改的变量。

我们来仔细分析一下这个程序的执行流程:

  1. 线程 t1 首先执行 a = 1。根据 volatile 变量规则,这个写操作 happens-before 于后面的 x = b
  2. 线程 t2 执行 b = 1。同样根据 volatile 变量规则,这个写操作 happens-before 于后面的 y = a
  3. 现在问题来了,x = by = a 谁先执行呢?这就不确定了。

根据 happens-before 规则,如果 t1a = 1 happens-before t2y = a,那么 y 一定能看到 a = 1的结果,即 y = 1

同理,如果 t2b = 1 happens-before t1x = b,那么 x 一定能看到 b = 1 的结果,即 x = 1

但是,如果 t1a = 1 不 happens-before t2y = a,那么 y 可能看到 a = 0(初始值),即 y = 0。同样,如果 t2b = 1 不 happens-before t1x = b,那么 x 可能看到 b = 0(初始值),即 x = 0

所以,最终的打印结果可能是:

x = 0, y = 0
x = 0, y = 1
x = 1, y = 0
x = 1, y = 1

这就是 happens-before 在多线程环境下的复杂性。它决定了一个线程能否看到另一个线程的执行结果,从而影响程序的正确性。掌握 happens-before 规则对于编写正确的并发程序至关重要。

happens-before 的应用场景

既然 happens-before 如此重要,那么它在实际应用中扮演着什么样的角色呢?让我们来看几个典型的应用场景。

1. 线程安全的发布

在多线程环境下,如何安全地发布一个共享对象是一个常见的问题。happens-before 规则可以帮助我们解决这个问题。

假设我们有一个 MyClass 对象,我们希望在一个线程中初始化它,然后在其他线程中使用它。我们可以利用 happens-before 规则来实现这个功能:

public class MyClass {
    private static MyClass instance;

    private MyClass() {
        // 初始化 MyClass
    }

    public static MyClass getInstance() {
        if (instance == null) {
            synchronized (MyClass.class) {
                if (instance == null) {
                    instance = new MyClass();
                }
            }
        }
        return instance;
    }
}

在这个例子中,我们使用了双重检查锁定 (Double-Checked Locking) 来实现线程安全的单例模式。关键在于, instance = new MyClass() 操作不仅仅是一个简单的赋值,它实际上包含了多个步骤:

  1. 分配内存空间
  2. 初始化 MyClass 对象
  3. instance 指向分配的内存空间

如果这三个步骤的执行顺序被重排,那么其他线程可能会观察到一个未初始化的 MyClass 对象。

幸运的是,happens-before 规则可以帮助我们解决这个问题。在 synchronized 块中,unlock 操作 happens-before 于后续的 lock 操作。这意味着,当一个线程成功获取到锁并执行 instance = new MyClass() 时,其他线程在 getInstance() 中的 instance == null 判断一定能看到已经正确初始化的 MyClass 对象。

2. 并发容器的实现

在 Java 并发编程中,并发容器是一个非常重要的概念。而 happens-before 规则在并发容器的实现中起着关键作用。

让我们以 ConcurrentHashMap 为例,看看 happens-before 是如何影响它的实现的:

public class ConcurrentHashMap<K, V> {
    private final Segment<K, V>[] segments;

    // ...

    public V put(K key, V value) {
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        return segmentFor(j).put(key, hash, value, false);
    }

    private Segment<K, V> segmentFor(int j) {
        return segments[j];
    }

    static final class Segment<K, V> extends ReentrantLock {
        volatile HashEntry<K, V>[] table;

        V put(K key, int hash, V value, boolean onlyIfAbsent) {
            lock();
            try {
                int c = count;
                if (c++ > threshold) // 扩容
                    rehash();
                HashEntry<K, V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K, V> first = tab[index];
                // ...
            } finally {
                unlock();
            }
        }
    }
}

ConcurrentHashMap 的实现中,每个 Segment 都是一个独立的锁段,用来保证并发安全。关键点在于:

  1. 当一个线程获取到 Segment 的锁后,它对 Segment 内部数据结构的修改对其他线程是可见的。这是因为 lock()unlock() 操作满足 happens-before 规则。
  2. 当一个线程扩容 Segment 的时候,新的 HashEntry 数组的发布对其他线程是可见的。这是因为扩容操作的 happens-before 关系。

通过 happens-before 规则的保证,ConcurrentHashMap 能够在多线程环境下安全地工作,避免出现数据不一致的问题。

3. 异步任务的协调

在实际项目中,我们经常需要编写一些异步任务,比如异步计算、异步通知等。这些任务之间通常存在着复杂的依赖关系,需要通过 happens-before 规则来协调它们的执行顺序。

举个例子,假设我们有一个异步计算任务,需要等待另一个异步任务的结果作为输入。我们可以利用 CompletableFuture 来实现这个需求:

CompletableFuture<Integer> taskA = CompletableFuture.supplyAsync(() -> {
    // 执行任务 A
    return 42;
});

CompletableFuture<String> taskB = taskA.thenApplyAsync(result -> {
    // 使用任务 A 的结果执行任务 B
    return "The answer is " + result;
});

String finalResult = taskB.join();

在这个例子中,taskB 的执行 happens-before 于 finalResult 的获取。这是因为 thenApplyAsync 方法保证了其回调函数的执行会在 taskA 完成之后。

通过 happens-before 规则,我们可以轻松地协调各个异步任务之间的依赖关系,确保程序的正确性。

  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值