美文网首页
2详解Happens-Before原则(解决并发编程可见性、有序

2详解Happens-Before原则(解决并发编程可见性、有序

作者: SuperMarry | 来源:发表于2019-09-21 23:28 被阅读0次

    并发的三个特性:原子性,可见性,有序性

    可见性 -> 缓存
    有序性 -> 编译优化

    volatile 使用

    介绍 volatile是在c语言的产物,他的本意是声明一个变量禁止使用cpu缓存。

    在 jdk1.5之后,volatile还被赋予了,局部禁止指令优化的功能,也就是对volatile 变量之前的操作对于再次访问volatile 变量时,必须是可见的。
    举个例子

    
    class Test {
    int x = 0;
    volatile boolean flag = false;
    //写线程A
    public void writer() {
    x = 42;
    flag = true;
    }
    //,读线程B,现在线程A完成了对flag的赋值,
    public void reader() {
    if (flag == true) {
    // 这里 x 会是多少呢?
    }
    }
    }
    
    

    假设现在有两个线程,写线程A,读线程B,现在线程A完成了对flag的赋值,flag = true,
    现在读线程B判断flag的值,为true,并且输出 x 的值,那么 x会是多少呢。
    在jdk1.5之前, x 的值可能是 0,也有可能是 42,因为对于 x 和 flag的赋值是可以优化的,具有不确定性,但是在jdk1.5之后,对于volatile增加了局部限制优化的功能,在flag之前的操作,也就是对 x 的赋值在再次访问 flag时,必须是完成可见的,所以jdk1.5之后,可以明确知道输出的值是42。

    Happens-Before原则
    对于Happens-BeforeHappens-Before原则,我们无需去尝试去翻译成中文理解其含义,我们要知道的是:Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则.
    Happens-Before原则总共有八条,这是并发编程中最为重要的原则,接下来一一分析。

    原则一:程序次序规则
    在一个线程之中,按照代码顺序,前面的操作对于后续操作是可见的。
    回到我们上边的例子,其实这个原则的最正确的说法应该是:在一个线程之中,按照代码顺序,看起来前面的操作对于后续操作是可见的。虽然最终执行的结果都一样,但是jvm还是会对代码进行指令重排,如 “x = 42; flag = true”,jvm可能先对 x 进行赋值,也可能对 flag进行赋值,但是对哪个变量进行操作对整个程序来说并没有什么区别。

    public void writer() {
    x = 42;
    flag = true;
    }
    

    假设我们这段代码变为

    public void writer() {
    a=13  //1
    x = 42;  //2
    flag = true; //3
    a=x*a; //4
    }
    

    jvm可能会对这段代码进行指令重排但是不管怎么重新排序,一定会在操作4之前,完成操作1,2,因为操作4对操作1和操作2有数据依赖。但是即使是优化,整段代码下来就跟程序顺序执行一样。
    在分析原则一时,我特意表明是在 一个线程中 ,那如果不在一个线程中会怎么样呢?
    class Test {
    int x = 0;
    boolean flag = false;
    //写线程A
    public void writer() {
    x = 42;
    flag = true;
    }
    //,读线程B,现在线程A完成了对flag的赋值,
    public void reader() {
    if (flag == true) {
    // 这里 输出的 x 会是多少呢?
    }
    }
    }
    在这里x输出的值可能是 0,也可能是42 。

    原则二:volatile变量规则
    对一个变量的写操作先行发生于后面对这个变量的读操作
    (在这里我们看不出原则二的作用是什么,我们结合原则三一起分析)

    原则三:传递规则
    如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C.
    我们回到文章开始的例子,这里将代码再贴一遍。

    class Test {
    int x = 0;
    volatile boolean flag = false;
    //写线程A
    public void writer() {
    x = 42;
    flag = true;
    }
    //,读线程B,现在线程A完成了对flag的赋值,
    public void reader() {
    if (flag == true) {
    // 这里 x 会是多少呢?
    }
    }
    }
    
    

    问:,写线程A,读线程B,现在线程A完成了对flag的赋值,flag = true,
    现在读线程B判断flag的值,为true,并且输出 x 的值,那么 x会是多少呢?
    在线程A中,通过volatile可以确保 x = 42 先行发生于 flag=true。
    在线程B中,通过原则二可以确保 flag==true 先行flag==true先行发生于 flag=true
    通过原则三就可以得出这样的顺序 x = 42 flag=true flag==true 所以最终输出的x值为
    42
    注:volatile可以确保 x = 42 先行发生于 flag=true是在jdk1.5之后的功能

    原则四:线程启动规则
    它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

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

    原则五:线程 join() 规则
    主线程 A 通过调用子线程 B 的 join() 方法实现,当子线程 B 完成后,主线程能够看到子线程对共享变量的的操作。

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

    原则七:锁定规则
    一个unLock操作先行发生于后面对同一个锁的lock操作

    
    synchronized (this) { 
    // x 是共享变量
    if (this.x < 12) {
    this.x = 12;
    }
    } 
    

    线程A占有此锁,线程B访问此段程序,B在要是能成功访问(获得锁),线程A必然已经释放锁。

    原则八:对象finalize规则
    一个对象的初始化完成先行发生于他的finalize()方法的开始。

    相关文章

      网友评论

          本文标题:2详解Happens-Before原则(解决并发编程可见性、有序

          本文链接:https://www.haomeiwen.com/subject/efhyuctx.html