Java并发编程 Happens-before简单的理解

4 篇文章 0 订阅

如果Java内存模型中所有的有序性都仅仅靠 volatile 和synchronized来完成,那么有一些操作将会变得很烦琐,但是我们在编写Java并发代码的时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生”(happens-before)的原则。这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。

happens-before 关系是用来描述和可见性相关的问题的。如果第一个操作happens-before第二个操作也可以描述为第一个操作和第二个操作之间满足 happens before 关系,那么我们说第一个操作对于第二个操作一定是可见的,也就是说第二个操作在执行时就一定能保证看见第一个操作执行的结果。

我们先来举一个不具备 happens before 关系的例子,从宏观上进一步理解 happens before 关系想要表达的内容。我们来看看下面的代码,代码很简单类,里面有一个 int x 变量,初始值为0,而 right 方法的作用是把 x 的值改写为一,而 read 方法的作用则是读取 x 的值。如果有两个线程分别执行 write 和 read 方法,那么由于这两个线程之间没有相互配合的机制,所以 write 和 read 方法内的代码不具备 happens-before 关系,其中的变量的可见性是无法保证的。

public class TestCurrency {

    int x = 0;

    public void write() {
        x = 1;
    }

    public void read() {
        int y = x;
    }
    
}

下面我们用例子说明这个问题,比如假设线程一已经先执行了 write 方法,修改了共享变量 x 的值,然后线程 2 执行 read 方法去读取 x 的值。此时我们并不能确定线程 2 现在是否能读取到之前线程一对于 x 所做的修改,线程 2 有可能看到本次修改,所以读取到的 x 的值是一,也有可能看不到本次修改,所以读取到的 x 的值是最初始的0。

既然存在不确定性,那么 write 和 read 方法内的代码就不具备 happens-before 关系。相反,如果第一个操作 happens-before 第二个操作,那么第一个操作对于第二个操作而言一定是可见的。下面我们来看一下 happens before 关系包含哪些具体的规则。

1. 单线程规则(Program Order Rule)

在一个单独的线程内,按照程序代码的顺序先执行的操作, happens-before 后执行的操作。也就是说,如果操作 x 和操作 y 是同一个线程的两个操作,并且在代码里 x 先于 y 出现,那么有 x happens before y,如下图所示。

这一个 happens-before 规则非常重要,因为如果对于同一个线程内部而言,后面的语句都不能保证可以看见前面的语句所执行的结果的话,那会造成非常严重的后果,程序的逻辑性就无法保证了。这里有一个注意点,那是不是意味着 happens before 关系的规则和重排序冲突?为了保证 happens before 关系就不能重排序了呢?答案是否定的。其实只要重排序后的结果依然符合 happens before 关系,也就是能保证可见性的话,那么就不会因此限制重排序的发生。

比如单线程内语句1在语句 2 的前面,所以根据单线程规则,语句1 happens before 语句2,但是这并不是说语句一定要在语句 2 之前被执行。

  • 例如语句一修改的是变量 a 的值,而语句 2 的内容和变量 a 无关,那么语句一和语句 2 依然有可能被重排序。
  • 如果语句一修改的是变量a,而语句 2 正好是去读取变量 a 的值,那么语句一就一定会在语句 2 之前执行了。

2. 锁操作规则(Monitor Locak Rule)

这包括 synchronized 和 lock 接口等。这个规则是如果操作 a 是解锁,而操作 b 是对同一个锁的加锁,那么 a happens-before b,如下图所示。

从上图可以看出,有线程 a 和线程 b 这两个线程 ,a先获取锁,b在等待获取锁,a 在加锁之后和解锁之前的所有操作,对于线程 b 而言都是可见的,这就是锁操作的 happens-before 关系的规则。

3. volatile 变量规则(Volatle Variable Rule)

对于一个 volatile 变量的写操作 happens-before 后面对该变量的读操作,这就代表了如果变量被 volatile 修饰,那么每次修改之后,其他线程在读取这个变量的时候一定能读取到该变量最新的值。 volatile 关键字保证可见性,而这正是由本条规则所规定的。

4. 线程启动规则(Thread Star Rule)

thread 对象的 start 方法 happens-before此线程 run 方法。里面的每一个操作如下图所示。

在图中的例子中,左侧区域是线程a,启动了一个子线程b,而右侧则是子线程b,那么子线程 b 在执行 run 方法里面的语句的时候,它一定能看到父线程在执行 thread.start() 之前的所有操作的结果。

5. 线程终止规则(Thread Termination Rule)

线程中的所有操作都先行发生于对此线制的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见,也称线程join()规则。

如下图所示,假设线程 a 通过调用 threadB.start() 启动了一个新线程b,然后调用 threadB.join()方法,那么线程 a 将一直等待到线程b的run方法结束,此处不考虑中断等特殊情况,然后 join 方法才会返回。在 join 方法返回后,线程 a 中的所有后续操作都可以看到线程 b run方法里面执行的所有操作的结果,也就是线程 b 的 run 方法里面的操作 happens-before 线程 a 的 join 之后的语句。

线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线制的终止检测,我们可以通过Thread.joinO)方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

6. 线程中断规则(Thread Interruption Rule)

对线程 interrupt 方法的调用 happens-before 检测该线程的中断事件。也就是说,如果一个线程被其他线程interrupt,那么在检测中断时,比如调用 Thread.interrupted(),或者是 Thread.isInterrupted()时,一定能够看到此次中断的发生,不会发生检测结果不准的情况。

7. 对象终结规则(Finalizer Rule)

一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。

8. 传递性规则(Transitviy)

如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值