重排序
重排序就是指的是编译器和处理器为了提高程序性能而对指令进行重新排序。
数据依赖性和as-if-serial语义
当两个指令有数据依赖的时候,也就是说重排序会影响程序执行的结果的时候,编译器和处理器就不会改变指令顺序,也就是as-if-serial语义:不管如何排序,程序执行的结果不能改变。
例如:
double pi = 3.14; //A
double r = 1; //B
double area = pi * r * r; //C
其中A和B没有数据依赖关系,可以进行重排序,先执行哪句都无所谓。 但是A和C,B和C都存在数据依赖关系,在执行C之前,A和B必须执行。
as-if-serial语义把单线程的程序保护起来,为了遵守这个语义, 编译器、runtime、处理器共同为编写单线程程序的程序员创建一个幻觉,就是程序按顺序来进行的。
在计算机中,软件技术和硬件技术都有一个共同的目标:在不改变程序执行结果的前提下,尽可能提高并行度。
重排序对多线程的影响
从上面可以看出,在单线程的程序中,重排序对程序的执行结果是没有改变的,但是在多线程的程序中,重排序则可能影响程序的执行结果:
class Example{
int a = 0;
boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a * a; //4
}
}
}
我们正常期待的结果肯定为1,但是程序执行的结果可能为0,也就是一个线程执行reader()方法的时候,应为1和2之间不存在数据依赖关系,可能进行重排序,也就是先执行2,后执行1,执行完2还没执行1的时候执行了reader(); 第四步的执行结果就为0。 同样,操作3和4也可能进行重排序。
顺序一致性
顺序一致性是一种理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性作为参考。
数据竞争与顺序一致性
java内存模型中对于数据竞争的定义:
1,一个线程写一个变量
2,一个线程读一个变量。
3,而且读和写没有进行同步。。
存在数据竞争的程序不具有顺序一致性。
顺序一致性模型
顺序一致性模型是一个被理想化的模型,顺序一致性的模型有两大特点:
1,一个线程中的所有操作必须顺序执行。
2,所有的线程都只能看到单一的执行顺序。 每个线程的每个操作都必须是原子操作,而且必须立即对所有线程可见。
但是在jvm中并没有顺序一致性模型的保证,未使用同步的程序不仅仅执行的顺序是没有顺序的,所有线程看到的顺序也不是一致的,因为有些线程的本地变量并没有刷新到主内存中。所以java中必须使用同步来实现顺序一致性模型。
例如上面的程序,可以使用如下的同步:
class Example{
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
}
}
}
在jvm中,临界区中的代码可以进行重排序,但是jvm不允许临界区代码逃逸到临界区之外,所谓临界区,可以理解为就是一个同步的代码块, 在上述程序中,1和2是可以进行重排序的, 3和4也还是可以进行重排序的,但是不会允许1和2执行之间插入别的操作,也就不会得出结果0 了,即用重排序保证了程序的性能,也有临界区的保护保证了程序的正确执行。【内存一致性模型】
在这里我们也可以看到,jvm在不改变程序正确执行的前提下,尽可能的为编译器和处理器优化打开了大门。
未同步程序的执行特性
对于没有同步的线程,jvm保证的是最小的安全性,也就是线程读取的变量,只可能是别的线程写入的或者是初始值。 为了保证这个安全性,jvm在堆内存中分配对象的时候回首先对内存空间进行清零,然后分配对象【jvm内部同步】。
jvm不保证对64位的long和double型的写操作有原子性。