深入理解java内存模型03-顺序一致性

深入理解java内存模型 -学习笔记
深入理解java虚拟机
JSR133
转载自并发编程网 本文链接地址: 深入理解Java内存模型

未正确同步的程序会表现出出人意料的行为

深入理解java内存模型03-顺序一致性_第1张图片

程序中用到了局部变量 r1 和 r2,以及共享变量 A 和 B。可能会出现 r2 == 2、 r1 == 1 这样的结果。
直觉上,应当要么指令 1 先执行要么指令 3先执行。

  • 如果指令 1 先执行,它不应该能看到指令 4 中写入的值。
  • 如果指令 3 先执行,它不应该能看到指令 2 写的值。
  • 从结果分析,可以得出程序执行的的顺序 4 -> 1 -> 2 -> 3 ,从表面看来,有悖常理。

然而,从单个线程的角度看,只要重排序不会影响到该线程的执行结果,编译器就可以对该线程中的指令进行重排序如果指令 1 与指令 2 重排序,那就很容易看出为什么会出现 r2 == 2 和 r1 == 1 这样的结果了。

一些编程人员可能认为程序表现出这种行为是不对的。但是,需要注意的是,这段代码没有被充分同步

  • 一个线程里有个写操作,
  • 另一个线程读取了这个写入的变量值,
  • 且读写操作没有被同步排序

当上述情况发生时,称之为存在数据争用(data race) 。当代码中存在数据争用时,常有可能出现有违直觉的结果。如果一个多线程程序能正确的同步,则它是一个没有数据竞争的程序,常用的同步原语如synchronized,volitale和final。

顺序一致性内存模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证,它有如下两个特性:

  • 一个线程中所有操作都必须按照程序的顺序来执行。
  • 每个动作都是原子的且立即对所有线程可见。

为了更好的理解,下面我们通过两个示意图来对顺序一致性模型的特性做进一步的说明。
假设有两个线程A和B并发执行。其中A线程有三个操作,它们在程序中的顺序是:A1->A2->A3。B线程也有三个操作,它们在程序中的顺序是:B1->B2->B3。

  • 假设这两个线程使用监视器来正确同步:A线程的三个操作执行后释放监视器,随后B线程获取同一个监视器。
    深入理解java内存模型03-顺序一致性_第2张图片
  • 假设这两个线程没有做同步
    深入理解java内存模型03-顺序一致性_第3张图片

未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程A和B看到的执行顺序都是:B1->A1->A2->B2->A3->B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。

但是在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致(非原子操作,指令重排)。


同步程序的顺序一致性效果

public class ReorderExample {
    int a = 0;
    boolean flag = false;

    public synchronized void writer() {
        a = 1;                   //1
        flag = true;             //2
    }

    public synchronized void reader() {
        if (flag) {                //3
            int i =  a * a;        //4
            System.out.println("i="+i);//打印 i = 0
        }

    }
}

上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图:
深入理解java内存模型03-顺序一致性_第4张图片

在顺序一致性模型中,所有操作完全按照程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序。(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM在退出临界区和进入临界区这两个关键时间点会做一些特别处理(当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中; 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。),使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。虽然线程A在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。
从这里我们可以看到JMM在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。


未同步程序的执行特性

对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false),JMM保证线程读操作读取到的值不会无中生有(out of thin air)的冒出来。为了实现最小安全性,JVM在堆上分配对象时,首先会清零内存空间,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在以清零的内存空间(pre-zeroed memory)分配对象时,域的默认初始化已经完成了。

JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为未同步程序在顺序一致性模型中执行时,整体上是无序的,其执行结果无法预知。未同步程序在顺序一致性模型和JMM有如下差异:

  • 顺序一致性模型保证单线程内的操作会按照程序的顺序执行,而JMM不保证单线程内的操作按程序的顺序执行(比如在正确同步的多线程程序在临界区内重排序)
  • 殊勋一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致性的操作执行顺序
  • JMM不保证64位long和double变量的读/写操作原子性,而内存一致性模型保证对所有的内存读/写都具有原子性

对于64位的数据类型(long,double),JMM特别定义一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作拆分为两次32位操作来进行。即允许虚拟机实现可以不保证64位数据类型的load,store,read和write 4个操作的原子性,这就是long和double的费原子协定(Nonatomic Treatment of long and double Variables).

在实际开发中,目前各平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此在编写代码的时候一般不需要把long和double变量声明为volitale.

你可能感兴趣的:(虚拟机)