Java并发编程之指令重排序

指令重排序

若是说内存可见性问题已经让你抓狂了,那么下边的这个指令重排序的事儿估计就要骂娘了~这事儿还得从一段代码提及:java

public class Reordering {

    private static boolean flag;
    private static int num;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                while (!flag) {
                    Thread.yield();
                }

                System.out.println(num);
            }
        }, "t1");
        t1.start();
        num = 5;
        flag = true;
    }
}

须要注意到flag并非一个volatile变量,也就是说它存在内存可见性问题,可是即使如此,num = 5也是写在flag = true的前边的,等到t1线程检测到了flag值的变化,num值的变化应该是早于flag值刷新到主内存的,因此线程t1最后的输出结果确定是5!!!编程

no!no!no! 输出的结果也多是0,也就是说flag = true可能先于num = 5执行,有没有亮瞎你的狗眼~ 这些代码最后都会变成机器能识别的二进制指令,咱们把这种指令不按书写顺序执行的状况称为指令重排序。大多数现代处理器都会采用将指令乱序执行的方法,在条件容许的状况下,直接运行当前有能力当即执行的后续指令,避开获取下一条指令所需数据时形成的等待。经过乱序执行的技术,处理器能够大大提升执行效率。安全

Within-Thread As-If-Serial Semantics

既然存在指令重排序这种现象,为何咱们以前写代码历来没感受到呢?到了多线程这才发现问题?多线程

指令重排序不是随便排,一个一万行的程序直接把最后一行当成第一行就给执行那不就逆天了了么,指令重排序是须要遵循代码依赖状况的。好比下边几行代码:并发

int i = 0, b = 0;
i = i + 5;  //指令1
i = i*2;  //指令2
b = b + 3;  //指令3

对于上边标注的3个指令来讲,指令2是对指令1有依赖的,因此指令2不能被排到指令1以前执行。可是指令3指令1指令2都没有关系,因此指令3能够被排在指令1以前,或者指令1指令2中间或者指令2后边执行均可以~ 这样在单线程中执行这段代码的时候,最终结果和没有重排序的执行结果是同样的,因此这种重排序有着Within-Thread As-If-Serial Semantics的含义,翻译过来就是线程内表现为串行的语义ide

可是这种指令重排序单线程中没有任何问题的,可是在多线程中,就引起了咱们上边在执行flag = true后,num的值仍然不能肯定是0仍是5性能

抑制重排序

在多线程并发编程的过程当中,执行重排序有时候会形成错误的后果,好比一个线程在main线程中调用setFlag(true)的前边修改了某些程序配置项,而在t1线程里须要用到这些配置项,因此会形成配置缺失的错误。可是java给咱们提供了一些抑制指令重排序的方式。优化

同步代码抑制指令重排序spa

将须要抑制指令重排序的代码放入同步代码块中:线程

public class Reordering {

    private static boolean flag;
    private static int num;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                while (!getFlag()) {
                    Thread.yield();
                }

                System.out.println(num);
            }
        }, "t1");
        t1.start();
        num = 5;
        setFlag(true);
    }

    public synchronized static void setFlag(boolean flag) {
        Reordering.flag = flag;
    }

    public synchronized static boolean getFlag() {
        return flag;
    }
}

在获取锁的时候,它前边的操做必须已经执行完成,不能和同步代码块重排序;在释放锁的时候,同步代码块中的代码必须所有执行完成,不能和同步代码块后边的代码重排序。

图片描述

加了锁以后,num=5就不能和flag=true的代码进行重排序了,因此在线程2中看到的num值确定是5,而不会是0喽~

虽然抑制重排序能够保证多线程程序按照咱们指望的执行顺序进行执行,可是它抑制了处理器对指令执行的优化,原来能并行执行的指令如今只能串行执行,会致使必定程度的性能降低,因此加锁只能保证在执行同步代码块时,它以前的代码已经执行完成,在同步代码块执行完成以前,代码块后边的代码是不能执行的,也就是只保证加锁前、加锁中、加锁后这三部分的执行时序,可是同步代码块以前的代码能够重排序,同步代码块中的代码能够重排序,同步代码块以后的代码也能够进行重排序,在保证执行顺序的基础上,尽最大可能让性能获得提高,比方说下边这段代码:

int i = 1;
int j = 2;
synchronized (Reordering.class) {
    int m = 3;
    int n = 4;
}
int x = 5;
int y = 6;

它的一个执行时序多是:

图片描述

volatile变量抑制指令重排序

仍是那句老话,加锁会致使竞争同一个锁的线程阻塞,形成线程切换,代价比较大,volatile变量也提供了一些抑制指令重排序的语义,上边的程序能够改为这样:

public class Reordering {

    private static volatile boolean flag;
    private static int num;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                while (!flag) {
                    Thread.yield();
                }

                System.out.println(num);
            }
        });
        t1.start();
        num = 5;
        flag = true;
    }
}
``
也就是把``flag``声明为``volatile变量``,这样也能起到抑制重排序的效果,``volatile变量``具体抑制重排序的规则以下:

1. volatile写以前的操做不会被重排序到volatile写以后。
2. volatile读以后的操做不会被重排序到volatile读以前。
3. 前边是volatile写,后边是volatile读,这两个操做不能重排序。
![图片描述][3]
除了这三条规定之外,其余的操做能够由处理器按照本身的特性进行重排序,换句话说,就是怎么执行着快,就怎么来。好比说:

flag = true;
num = 5;
``
volatile变量以后进行普通变量的写操做,那就能够重排序喽,直到遇到一条volatile读或者有执行依赖的代码才会阻止重排序的过程。

final变量抑制指令重排序

在java语言中,用final修饰的字段被赋予了一些特殊的语义,它能够阻止某些重排序,具体的规则就这两条:

  1. 在构造方法内对一个final字段的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。
  2. 初次读一个包含final字段对象的引用,与随后初次读这个final字段,这两个操做不能重排序。

可能你们看的有些懵逼,赶忙写代码理解一下:

public class FinalReordering {

    int i;
    final int j;

    static FinalReordering obj;

    public FinalReordering() {
        i = 1;
        j = 2;
    }

    public static void write() {
        obj = new FinalReordering();
    }

    public static void read() {
        FinalReordering finalReordering = FinalReordering.obj;
        int a = finalReordering.i;
        int b = finalReordering.j;
    }
}

咱们假设有一个线程执行write方法,另外一个线程执行read方法。

先看一下对final字段进行写操做时,不一样线程执行write方法和read方法的一种可能状况是:

图片描述

从上图中能够看出,普通的字段可能在构造方法完成以后才被真正的写入值,因此另外一个线程在访问这个普通变量的时候可能读到了0,这显然是不符合咱们的预期的。可是final字段的赋值不容许被重排序到构造方法完成以后,因此在把该字段所在对象的引用赋值出去以前,final字段确定是被赋值过了,也就是说这两个操做不能被重排序

再来看一下初次读取final字段的状况,下边是不一样线程执行write方法和read方法的一种可能状况:
图片描述

从上图能够看出,普通字段的读取操做可能被重排序到读取该字段所在对象引用前边,天然会获得NullPointerException异常喽,可是对于final字段,在读final字段以前,必须保证它前边的读操做都执行完成,也就是说必须先进行该字段所在对象的引用的读取,再读取该字段,也就是说这两个操做不能进行重排序

值得注意的是,读取对象引用与读取该对象的字段是存在间接依赖的关系的,对象引用都没有被赋值,还读个锤子对象的字段喽,通常的处理器默认是不会重排序这两个操做的,但是有一些为了性能不顾一切的处理器,好比alpha处理器,这种处理器是可能把这两个操做进行重排序的,因此这个规则就是给这种处理器贴身设计的~ 也就是说对于final字段,无论在什么处理器上,都得先进行对象引用的读取,再进行final字段的读取。可是并不保证在全部处理器上,对于对象引用读取和普通字段读取的顺序是有序的。

安全性小结

咱们上边介绍了原子性操做内存可见性以及指令重排序三个在多线程执行过程当中会影响到安全性的问题。

  • synchronized能够把三个问题都解决掉,可是伴随着这种万能特性,是多线程在竞争同一个锁的时候会形成线程切换,致使线程阻塞,这个对性能的影响是很是大的。
  • volatile不能保证一系列操做的原子性,可是能够保证对于一个变量的读取和写入是原子性的,一个线程对某个volatile变量的写入是能够当即对其余线程可见的,另外,它还能够禁止处理器对一些指令执行的重排序。
  • final变量依靠它的禁止重排序规则,保证在使用过程当中的安全性。一旦被赋值成功,它的值在以后程序执行过程当中都不会改变,也不存在所谓的内存可见性问题。
相关文章
相关标签/搜索