重排序引起的内存可见性问题

  • 什么是重排序
  • 什么是内存可见性
  • 将产生的问题
  • 如何解决问题

什么是重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种优化措施

如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作之间就存在数据依赖性数据依赖性可概括为以下三种类型:
这里写图片描述
上面三种情况,只要重排序两个操作的执行顺序,程序的结果就会被改变。在Java内存模型(以下简称JMM)中,为了效率是会对程序进行重排序。只有满足某些条件的时候,JMM才会禁止这些重排序,比如使用具有同步语义的语句等等。

什么是内存可见性

如果线程A对共享变量X进行了修改,但是线程A没有及时把更新后的值刷入到主内存中,而此时线程B从主内存读取共享变量X的值,所以X的值是原始值,那么我们就说对于线程B来讲,共享变量X的更改对线程B是不可见的。

在以上叙述中要注意这么一句话:对于线程B来讲,共享变量X的 更改 对线程B是不可见的。那么线程,线程工作内存和主内存之间可以用下图来表示:

线程,工作内存与主内存之间的关系

将产生的问题

看一段《Java Concurrency in Practice》中的代码

public class PossibleReordering {
    static int x = 0, y = 0;
    static int a = 0, b = 0;
    public static void main(String[] args)throws InterruptedException {
            Thread one = new Thread(new Runnable() {//线程A
                        public void run() {
                            a = 1;//step 1
                            x = b;//step 2
                        }
                            });
            Thread other = new Thread(new Runnable() {//线程B
                        public void run() {
                            b = 1;//step 3
                            y = a;//step 4
                        }
                            });
            one.start(); other.start();
            one.join(); other.join();
            System.out.println("( "+ x + "," + y + ")");
    }
}

在以上代码运行中,如果两个线程没有正确的进行同步,我们很难说清楚最后的结果是什么。有可能输出:(1, 0), or (0, 1), or (1, 1)甚至(0,0)的情况,为什么呢?这是因为JVM重排序的结果,重排序会使得step1到step4执行的顺序无法预测,这取决于JVM的优化策略。由于JMM采用是共享内存模型,而非顺序一致性模型,所以未同步的程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。在JMM中,,当前线程把写过的数据缓存在本地内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见,从其它线程角度来看,会认为这个写操作根本没有被当前线程执行,即是说,只有当前线程把本地内存写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。所以其它线程根本不知道有线程对共享资源正进行修改,更不会去等待其修改完毕再去从主内存取。

如何解决问题

已经知道了产生这种问题的原因是因为重排序,那么解决问题自然就从禁止重排序上着手。

Happen-before规则:

  • x Program order rule. Each action in a thread happensͲbefore every action in that thread that comes later in the program order.
  • * 程序顺序规则:线程里的每一个操作都先行于代码编写顺序中后来的线程里的每一个操作(注意是线程之间的顺序,在有些中译本书籍中翻译容易被误导)*
  • x Monitor lock rule. An unlock on a monitor lock happensͲbefore every subsequent lock on that same monitor lock.
  • * 监视器锁规则:一个监视器锁的释放先行与每个相同监视器锁的加锁操作(每次都是先解锁在获取锁,而不是先获取锁,在把已经拥有的锁释放掉)*
  • x Volatile variable rule. A write to a volatile field happensͲbefore every subsequent read of that same field.
  • * volatile变量规则:对volatile域变量的写操作先行于每一个后来的对该变量的读操作(这里要注意写操作是指刷新到内存,读操作指的是从主内存读,线程修改操作和写操作是两码事) *
  • x Thread start rule. A call to Thread.start on a thread happensͲbefore every action in the started thread.
  • * 线程启动规则:线程的start方法先行于,线程对象里的每一个操作,比如run()*
  • x Thread termination rule. Any action in a thread happensͲbefore any other thread detects that thread has terminated, either by successfully return from Thread.join or by Thread.isAlive returning false .
  • * 线程终止规则:线程里的每一个操作先行于其它线程检测到该线程已结束,或者该线程成功的从Thread.join方法返回,或者该线程Thread.alive返回false(其它线程检测到A线程down掉了,可是A线程里面还在执行操作,这是不允许的)*
  • x Interruption rule. A thread calling interrupt on another thread happensͲbefore the interrupted thread detects the interrupt (either by having InterruptedException thrown, or invoking isInterrupted or interrupted ).
  • * 中断规则:一个线程调用另一个线程的中断方法先行于被中断线程检测到中断(比如,抛出了中断异常,或者调用 isInterrupted or interrupted)*
  • x Finalizer rule. The end of a constructor for an object happensͲbefore the start of the finalizer for that object.
  • * 终接器规则:对象的构造函数必须在启动该对象的总结器之前执行完成*
  • x Transitivity. If A happensͲbefore B, and B happensͲbefore C, then A happensͲbefore C.
  • * 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;*

从JDK1.5之后,JMM使用happen-before的概念阐述操作之间的内存可见性。并保证只要线程A和B之间满足happen-before关系,执行操作B的线程可以看到操作A的结果,本质在于JMM使用内存屏障禁止了操作重排序,从而实现一种偏序关系。
如何在编程中如何保证这种偏序Happen-before关系呢?
使用同步操作:同步操作满足全序关系的,所以一定满足偏序关系。同步操作一般有:锁的获取与释放、对volatile变量的读和写
在volatile方案下能保证:step1 > step2 和 step3>step4
那么就有这几种情况发生:
1>2>3>4(0,1)
1>3>2>4(1,1)
1>3>4>2(1,1)
3>1>2>4(1,1)
3>1>4>2(1,1)
3>4>1>2(1,0)

public class PossibleReordering {
    static volatile int x = 0, y = 0;//使用volatile 解决方案
    static volatile int a = 0, b = 0;//使用volatile 解决方案
    public static void main(String[] args)throws InterruptedException {
            Thread one = new Thread(new Runnable() {//线程A
                        public void run() {
                            a = 1;//step 1
                            x = b;//step 2
                        }
                            });
            Thread other = new Thread(new Runnable() {//线程B
                        public void run() {
                            b = 1;//step 3
                            y = a;//step 4
                        }
                            });
            one.start(); other.start();
            one.join(); other.join();
            System.out.println("( "+ x + "," + y + ")");
    }
}

值得一提的是,案例中把所有的变量都设置为volatile,其实若对volatile重排序规则了解的话,可以知道大可不必这样,因为插入内存屏障会有所开销:po一张volatile排序规则表:
这里写图片描述
在这张表里可以看出其实只需要把x和y设为volatile即可

    static volatile int x = 0, y = 0;//使用volatile 解决方案
    static int a = 0, b = 0;

使用synchronized方案:
step1 > step2 > step3 > step4
step1 > step2 > step4 > step3
step2 > step1 > step4 > step3
step2 > step1 > step3 > step4

step3 > step4 > step1>step2
step3 > step4 > step2>step1
step4 > step3 > step2>step1
step4 > step3 > step2>step1
结果只有(0,1)和(1,0)
从这里可以看出,12之间并没有数据依赖关系,34之间也是同样。其实该方案遵循的happen-before的程序顺序规则

public class PossibleReordering {
    static Integer x = 0, y = 0;//使用synchronized 解决方案
    static Integer a = 0, b = 0;//使用synchronized 解决方案
    public static void main(String[] args)throws InterruptedException {
            Thread one = new Thread(new Runnable() {//线程A
                        public void run() {
                            synchronized (a){
                                a = 1;//step 1
                                x = b;//step 2
                }
                        }
                            });
            Thread other = new Thread(new Runnable() {//线程B
                        public void run() {
                            synchronized (a){
                                b = 1;//step 3
                                y = a;//step 4
              }
                        }
                            });
            one.start(); other.start();
            one.join(); other.join();
            System.out.println("( "+ x + "," + y + ")");
    }
}

利用final域重排序规则:

对于final域,编译器和处理器要遵守两个重排序规则:

-在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
-初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

final域重排序规则多用于对象构造,避免产生未初始化完全的对象。将会在《如何安全的发布一个对象》一文中加以讲解

展开阅读全文

没有更多推荐了,返回首页