【java】Java 原子性、有序性与Happens-Before

412 篇文章 483 订阅 ¥19.90 ¥99.00
本文探讨Java并发编程中的原子性、有序性问题,以及Happens-Before原则。通过实例分析了线程切换导致的原子性问题、编译优化引发的有序性问题,并详细阐述了Java内存模型和Happens-Before的八大规则,以保证并发安全。
摘要由CSDN通过智能技术生成


在这里插入图片描述

1.概述

转载:Java核心复习—— 原子性、有序性与Happens-Before

视频:视频教程

2.产生并发Bug的源头

  • 可见性 缓存导致的可见性问题
  • 原子性 线程切换带来的原子性问题
  • 有序性 编译优化带来的有序性问题

上面讲到了 volatile 与可见性,本章再主要讲下原子性、有序性与Happens-Before规则。

3.线程切换带来的原子性问题

在这里插入图片描述
count += 1 这一句高级语言的语句,往往需要多条CPU执令。可以分为3步:

  • 将count值加载到寄存器
  • 在寄存器中对count进行+1操作
  • 将count值写回内存

在这里插入图片描述
所以,我们需要在高级语言的层面上,确保一些操作是原子性操作。

三、编译优化带来的有序性问题#

编译器为了优化性能,有时会改变程序中语句的先后顺序。

a = 6;
b = 7;

经过编译优化后,可能会变成

b = 7;
a = 6

双重检查创建单例对象

public class Singleton{

    static Singleton instance;
    static Singleton getInstance(){
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
    }

}

这个例子看似很完美,但其实是可能触发空指针异常。

为什么可能会触发空指针异常。

假设getInstance()的运行过程是这样:

  • 开辟一块M内存空间
  • 在M内存空间上创建Singleton对象
  • 将对象赋值给instance

这样的话是没问题的。但编译时并非按这个顺序来的,而是按照下面的顺序来:

  • 开辟一块M内存空间
  • 将M内存空间的地址赋值给instance
  • 在M内存空间创建Singleton对象。

当A线程走到了第2步,将M内空空间的地址赋值给instance时,发生线程切换,则B线程判断instance == null时,结果返回false,则返回null,导致返回空指针。

在这里插入图片描述

四、Java内存模型

上面说到产生并发Bug的源头是缓存导致的可见性、编译优化导致的顺序性。如果禁用缓存和编译优化是不是就问题解决了,并不是,将引入最大的问题,程序性能问题。

合理的方案是按需求来禁用缓存和编译优化。

Java内存模型规范了按需禁用缓存和编译优化的方法(volatile、synchronized、final、Happens-Before规则)。


class VolatileExample{

    int x = 0;

    volatile boolean v = false;

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

    publci void reader(){
        if(v == true){
            //x = ?
            sout(x);
        }
    
    }

}


上面的例子,A线程调用writer(),B线程调用reader(),B看到的x是多少?JDK1.5以前是0,JDK1.5及以上是42。

原因是JDK1.5对volatile进行增强,新增了Happens-Before规则。

五、Happens-Before规则

在Java语言中,有一个先行发生原则(happens-before)。它包括八大规则,如下:

  1. 程序次序规则 :在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。

  2. 管程锁定规则 :一个unLock操作先行发生于后面对同一个锁额lock操作

  3. volatile变量规则 :对一个变量的写操作先行发生于后面对这个变量的读操作

  4. 线程启动规则 :Thread对象的start()方法先行发生于此线程的每个一个动作

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

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

  7. 对象终结规则 :一个对象的初始化完成先行发生于他的finalize()方法的开始

  8. 传递性 :如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

5.1 规则1:程序的顺序性规则

意思就是:前面一个操作的结果对后续操作是可见的。

x = 42 ; Hapens-Before于 v = true;

如果是在JDK1.5以前,v = true可能会先被执行。


class VolatileExample{

    int x = 0;

    volatile boolean v = false;

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

    publci void reader(){
        if(v == true){
            //x = ?
            sout(x);
        }
    
    }

}


5.2 规则2:volatile变量规则#

对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。

5.3 规则3:传递性#

A Happens-Before B
B Happens-Before C

那么

A Happens-Before C

class VolatileExample{

    int x = 0;

    volatile boolean v = false;

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

    publci void reader(){
        if(v == true){
            //x = ?
            sout(x);
        }
    
    }

}


规则2结合规则3和规则1来一起看。

x = 42 Happens-Before v = true,这是规则1

A线程写变量v=true Happens-Before B线程读变量v=true,这是规则2
由规则1、2结合规则3的传递性,得出x = 42 Happens-Before B线程读变量v=true

在这里插入图片描述

5.4 规则4:管程中锁的规则

对一个锁的解锁 Happens-Before于后续对这个锁的加锁。

管程是一种通用的同步原语。同步原语是什么。。。。synchronized是Java对管程的实现。


synchronized(this){//此处自动加锁
    // x 是共享变量,初始值=10

    if(this.x < 12){

        this.x = 12;
    }

}//此处自动解锁


A执行完代码块后x=12,执行完释放锁。线程B进入代码块,能够看到A对x的写操作。

5.5 规则5:线程start()规则

主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B之前的操作。

也就是 start()操作 Happens-Before 线程B中的任意操作。



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

var = 77;
B.start();


5.6 规则6:线程join()规则

这条是关于线程等待的。主线程A等待子线程B完成(A调用子线程B的join()方法)。当子线程B完成后,主线程能够看到子线程的操作。



Thread B = new Thread(()-> {
    
    var = 66;

})

//一系列操作


B.start();

//进行一系列操作


B.join()


线程B中的任意操作,Happens-Before 于该join()操作。

六、final

final修饰变量时,初衷是告诉编译器:这个变量生而不变,可以尽可能的优化。

那什么时候使用final呢?

一个答案就是“尽可能的使用”。任何你不希望改变的(基本类型,或者指向一个对象,不管该对象是否可变)一般来讲都应该声明为final。

另一种看待此问题的方式是:

如果一个对象将会在多个线程中访问并且你并没有将其成员声明为final,则必须提供其他方式保证线程安全

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值