zoukankan      html  css  js  c++  java
  • Java核心复习—— 原子性、有序性与Happens-Before

    一、 产生并发Bug的源头

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

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

    二、线程切换带来的原子性问题

    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()的运行过程是这样:

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

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

    1. 开辟一块M内存空间
    2. 将M内存空间的地址赋值给instance
    3. 在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规则

    规则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);
            }
        
        }
    
    }
    
    
    

    规则2:volatile变量规则

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

    规则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

    传递性规则

    规则4:管程中锁的规则

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

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

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

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

    规则5:线程start()规则

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

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

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

    规则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,则必须提供其他方式保证线程安全

    七、并发编程的学习路线图

    参考文档

    《Java并发编程实战——王宝令》
    JSR 133 (Java Memory Model) FAQ
    关于java中final关键字与线程安全性

  • 相关阅读:
    linux网络编程系列TCP及常用接口分析
    Linux网络编程系列TCP状态分析
    常见的HTTP 1.1状态代码及含义
    修改android SDK 模拟器(avd) 内存大小
    Android应用研发核心竞争力
    网路编程——阻塞式&&非阻塞式
    URI、URL和URN之间的区别与联系
    初识android——四大组件
    无依赖的combobox组件(autocomplete组件)
    为什么JS没有catchMyException或类似的方法
  • 原文地址:https://www.cnblogs.com/fonxian/p/10879007.html
Copyright © 2011-2022 走看看