Java并发编程(四)有序性

1,498 阅读5分钟

banner窄.png

铿然架构  |  作者  /  铿然一叶 这是铿然架构的第 34 篇原创文章

相关阅读:

Java并发编程(一)知识地图
Java并发编程(二)原子性
Java并发编程(三)可见性
Java并发编程(五)创建线程方式概览
Java并发编程入门(六)synchronized用法
Java并发编程入门(七)轻松理解wait和notify以及使用场景
Java并发编程入门(八)线程生命周期
Java并发编程入门(九)死锁和死锁定位
Java并发编程入门(十)锁优化
Java并发编程入门(十一)限流场景和Spring限流器实现
Java并发编程入门(十二)生产者和消费者模式-代码模板
Java并发编程入门(十三)读写锁和缓存模板
Java并发编程入门(十四)CountDownLatch应用场景
Java并发编程入门(十五)CyclicBarrier应用场景
Java并发编程入门(十六)秒懂线程池差别
Java并发编程入门(十七)一图掌握线程常用类和接口
Java并发编程入门(十八)再论线程安全
Java并发编程入门(十九)异步任务调度工具CompleteFeature
Java并发编程入门(二十)常见加锁场景和加锁工具


1. 关于有序性

1.1. 定义

在代码顺序结构中,可以直观的指定代码的执行顺序, 即从上到下按序执行。但编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序。优化指令的执行顺序,提升程序的性能和执行速度,使语句执行顺序发生改变,出现重排序,但最终结果看起来没什么变化(单核)。

1.2. 问题

在多线程环境下(多核),由于执行语句重排序后,重排序的这一部分还没有一起执行完,就切换到了其它线程,导致结果与预期不符。这就是编译器的编译优化给并发编程带来的程序有序性问题。

2. 有序性问题原因-指令重排

在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。

代码例子:

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

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

        return singleton;
    }
}

对于如下代码:

singleton = new Singleton();

我们以为的顺序是:
1.分配一块内存
2.在内存上初始化Singleton对象
3.然后M的地址赋值给instance变量

但实际上不是,查看JAVA字节码:

public class com.javashizhan.concurrent.demo.base.Singleton {
  public static com.javashizhan.concurrent.demo.base.Singleton getInstance();
    Code:
       0: aconst_null
       1: getstatic     #2                  // Field singleton:Lcom/javashizhan/concurrent/demo/base/Singleton;
       4: if_acmpne     39
       7: ldc           #3                  // class com/javashizhan/concurrent/demo/base/Singleton
       9: dup
      10: astore_0
      11: monitorenter
      12: aconst_null
      13: getstatic     #2                  // Field singleton:Lcom/javashizhan/concurrent/demo/base/Singleton;
      16: if_acmpne     29
      19: new           #3                  // class com/javashizhan/concurrent/demo/base/Singleton
      22: dup
      23: invokespecial #4                  // Method "<init>":()V
      26: putstatic     #2                  // Field singleton:Lcom/javashizhan/concurrent/demo/base/Singleton;
      29: aload_0
      30: monitorexit
      31: goto          39
      34: astore_1
      35: aload_0
      36: monitorexit
      37: aload_1
      38: athrow
      39: getstatic     #2                  // Field singleton:Lcom/javashizhan/concurrent/demo/base/Singleton;
      42: areturn
    Exception table:
       from    to  target type
          12    31    34   any
          34    37    34   any
}

看19~26,实际的顺序是:
1.分配一块内存
2.将M的地址赋值给instance变量
3.最后在内存M上初始化Singleton对象。

由于指令的顺序问题,多线程并发时线程A执行到26之前发生了线程切换,此时线程B发现null == singleton不成立,获取到singleton,而此时singleton并没有初始化完,就会引发空指针异常。

3. Happens-Before规则

Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守Happens-Before 规则。

1.程序的顺序性规则
在一个线程中,按照程序顺序,前面的操作Happens-Before于后续的任意操作。程序前面对某个变量的修改一定是对后续操作可见的。

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

3.传递性 如果A Happens-Before于B,B Happens-Before于C,那么A Happens-Before于C。

4.锁的规则
对一个锁的解锁Happens-Before于对这个锁的加锁。

5.线程Start规则 主线程A启动子线程B后,子线程B能看到主线程在启动子线程B之前的操作。

6.线程join规则
主线程A等待子线程B完成(主线程通过调用子线程B的join方法实现),当子线程B完成后(主线程A中join()方法返回),主线程能够看到子线程的操作。

7.线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

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

4. 解决有序性问题

1.通过volatile修饰变量避免指令重排。注意:避免指令重排并不表示线程安全。
2.加锁操作。


<--阅过留痕,左边点赞 !