《菜鸟读并发》java内存模型之happen-before

面试题:happen-before的八个基本规则你知道吗?

《菜鸟读并发》java内存模型之happen-before_第1张图片

Happens-Before

Happens-Before真正要表达的是:前面一个操作的结果对后续操作是可见的

就像有心灵感应的两个人,虽然远隔千里,一个人心之所想,另一个人都看得到。Happens-Before 规则就是要保证线程之间的这种“心灵感应”。

Happens-Before原则它是判断数据是否存在竞争线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题

所以比较正式的说法是:Happens-Before约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens- Before 规则。

在java内存模型(JMM)中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

八个基本规则

Happens-Before程序员相关的规则一共有如下八项,都是关于可见性的。

1. 程序的顺序性规则

这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before于后续的任意操作

比如下面的示例代码,按照程序的顺序,“x = 42;” Happens-Before 于代码 v = x;”,这就是规则1的内容,也比较符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。

class VolatileExample { 
    int x = 0; 
    volatile int v = 10; 
    public void writer() { 
        x = 42;
        v = x; 
    } 
    } 
 }

2. volatile 变量规则

这条规则是指对一个volatile变量的写操作,Happens-Before 于后续对这个volatile变量的读操作

这个就有点费解了,对一个volatile变量的写操作相对于后续对这个 volatile 变量的读操作可见,这怎么看都是禁用缓存的意思啊,貌似和 1.5 版本以前的语义没有变化啊?如果
单看这个规则,的确是这样,但是如果我们关联一下规则 3,就有点不一样的感觉了。

3. 传递性

这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么A Happens-Before C。

下面我们结合具体的代码,我们利用这条规则推导下

public class VolatileExample {
    private int a = 0;
    private volatile boolean flag = false;
    public void writer(){
        a = 1;          //1
        flag = true;   //2
    }
    public void reader(){
        if(flag){      //3
            int i = a; //4
        }
    }
}

我们假设这里有线程A和线程B,线程A先执行writer方法,然后再线程B执行reader方法。我们将规则3的传递性和规则2和规则1应用到我们的例子中,会发生什么呢?可以看下面这幅图:
《菜鸟读并发》java内存模型之happen-before_第2张图片

从图中,我们可以看到:

  1. 根据程序顺序规则推导出来黑色的线条
  2. 根据volatile变量规则推导出来红色的线条,volatile写happens-before 于任意后续对volatile变量的读,也就是说当线程A将volatile变量 flag更改为true后线程B就能够迅速感知,是可见的
  3. 根据传递性规则推导出来蓝色的线条,我们得到结果:“a=1” Happens-Before读变量“ int i = a;”,这就是 1.5 版本对volatile 语义的增强,这个增强意义重大,1.5版本的并发工具包(java.util.concurrent)就是靠volatile语义来搞定可见性的,这个在后面的内容中会详细介绍

分析完前三个happens-before关系后我们现在就来进一步分析volatile的内存语义,还是以上面的代码为例,假设线程A先执行writer方法,线程B随后执行reader方法,初始时线程的本地内存中flag和a都是初始状态,下图是线程A执行volatile写后的状态图。
《菜鸟读并发》java内存模型之happen-before_第3张图片
当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图
《菜鸟读并发》java内存模型之happen-before_第4张图片
从横向来看,线程A和线程B之间进行了一次通信,线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B你现在的值都是旧的了,然后线程B读这个volatile变量时就像是接收了线程A刚刚发送的消息。既然是旧的了,那线程B自然而然就只能去主内存去取啦

4. 管程中锁的规则

这条规则是指对一个锁的解锁Happens-Before于后续对这个锁的加锁

要理解这个规则,就首先要了解管程指的是什么。管程是一种通用的同步原语,在Java中指的就是 synchronized,synchronized是Java里对管程的实现。管程中的锁在Java里是隐式实现的.

例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。当然Lock中也是一样,一个unLock操作先行发生于后面对同一个锁的lock操作

 synchronized (this) { 
     // 此处自动加锁 
     // x 是共享变量, 初始值 =10 
     if (this.x < 12) { 
      this.x = 12; 
      } 
  } 
  // 此处自动解锁

所以结合规则 4管程中锁的规则,可以这样理解:假设 x 的初始值是 10,线程 A 执行完代码块后 x的值会变成 12(执行完自动释放锁),线程B进入代码块时,能够看到线程 A 对 x的写操作,也就是线程B能够看到x==12。这个也是符合我们直觉的,应该不难理解。

5. 线程 start() 规则

这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程B前的操作

换句话说就是,如果线程A调用线程B的start()方法(即在线程 A 中启动线程B),那么该start()操作Happens-Before 于线程 B中的任意操作。具体可参考下面示例代码

Thread B = new Thread(()->{ 
    // 主线程调用 B.start() 之前 
    // 所有对共享变量的修改,此处皆可见 
    // 此例中,var==77 
}); 
// 此处对共享变量 var 修改 
var = 77; 
// 主线程启动子线程 
B.start();

6. 线程中断规则

线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

7. 线程终结规则

这条是关于线程等待的,线程中所有的操作都先行发生于线程的终止检测.

我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。

例如它是指主线程A等待子线程B完成(主线程A通过调用子线程B的join()方法实现),当子线程B完成后(主线程A中join()方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。换句话说就是,如果在线程A中,调用线程B的join()并成功返回,那么线程B中的任意操作Happens-Before于该join()操作的返回。具体可参考下面示例代码。


Thread B = new Thread(()->{ 
    // 此处对共享变量 var 修改 
    var = 66; 
}); 
// 例如此处对共享变量修改, 
// 则这个修改结果对线程 B 可见 
// 主线程启动子线程 8 B.start(); 
B.join() 
// 子线程所有对共享变量的修改 
// 在主线程调用 B.join() 之后皆可见 
// 此例中,var==66

8. 对象终结规则

一个对象的初始化完成先行发生于他的finalize()方法的开始

其他规则

上面八条是原生Java满足Happens-before关系的规则,但是我们可以对他们进行推导出其他满足happens-before的规则:

  1. 将一个元素放入一个线程安全的队列的操作Happens-Before从队列中取出这个元素的操作
  2. 将一个元素放入一个线程安全容器的操作Happens-Before从容器中取出这个元素的操作
  3. 在CountDownLatch上的倒数操作Happens-Before CountDownLatch#await()操作
  4. 释放Semaphore许可的操作Happens-Before获得许可操作
  5. Future表示的任务的所有操作Happens-Before Future#get()操作
  6. 向Executor提交一个Runnable或Callable的操作Happens-Before任务开始执行操作

这里再说一遍happens-before的概念:

如果两个操作不存在上述(前面8条+后面6条)任一一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

小结

happen-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。

《菜鸟读并发》java内存模型之happen-before_第5张图片

如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。 感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

原创不易,欢迎转发,关注公众号“码农进阶之路”,获取更多面试题,源码解读资料!
在这里插入图片描述

你可能感兴趣的:(Java,JUC并发编程系列,菜鸟读并发)