volatile
volatile只能保证变量的可见性,有序性,却不能保证原子性。
- 可见性:当一个线程修改了被volatile修饰的变量后,会马上通知其他线程,那么其他线程在读的时候,都会读到最新的变量值。
- 有序性:volatile通过内存屏障禁止了指令间的重排序(当指令或者操作间没有依赖关系时,编译器可能会通过指令或操作重排序,提高程序性能),详见下文。
- 原子性:如i++这类涉及多个操作语句,volatile是无法保证这类操作的正确性的,如果想保证原子性,可以用Atomic原子类里的对象,也可以直接对变量加锁。
JMM
Java并发采用共享内存模型。对类中对一个成员变量来说,它是存储在主内存中的,每个线程要操作这个变量时,都会拷贝一份它的副本,到线程自己的工作内存中,等操作完了,再将操作后的变量的值,刷新回主内存中。
上述操作通过8个内存指令完成:
- lock:将一个变量标识为线程独占状态
- read:将变量的值读到线程的工作内存中
- load:创建一个变量副本,将读到的值存储
- use:线程可以使用这个变量副本了
- assign:线程操作这个变量副本
- store:将修改后的变量值刷新回主内存中
- write:将刷新回来的值存储到变量中,完成变量的更新
- unlock:解除变量的独占状态
其中通过load和store指令的结合,形成了四种内存屏障(loadload,loadstore,storeload,storestore),volatile通过在上述流程中插入这四种内存屏障,禁止了可能会发生的指令重排序(具体怎么插入,禁止的流程,见《Java并发编程的艺术》)。
注:JMM不保证对64位的long和double的写操作有原子性,会将他们拆成两个32位的“半数”处理,目的是为了兼容以前32位的系统。所以针对这两种类型的数,建议一定要用Atomic包下面的原子类来声明。
happens-before和as-if-serial
在执行程序时,为提高性能,编译器和处理器常常会对代码做重排序。然而具有happens-before和as-if-serial关系的操作,是无法被重排序的。
- happens-before:如果一个操作的执行需要对另一个操作可见(不论是在一个线程里还是跨线程),那么两个操作之间存在happens-before关系,无法被重排序(详情见《Java并发编程的艺术》)。
- as-if-serial:保证了不论代码如何重排序,单线程程序的执行结果总是不变的。
如:int a = 1; int b = 1; int c = a + b;
我们发现,代码1和代码2不论谁先执行,对结果都没有影响,而代码3却不能先于代码1和代码2执行,所以最终程序的执行序列可能是1->2->3或者2->1->3。 as-if-serial给人一种错觉:单线程程序是按照程序书写的顺序执行的,实际上是遵守了这一关系,并不是一定按顺序执行的。
在多线程中,对上述指令有1 happens-before 3, 2 happens-before 3,而代码1和代码2之间没有happens-before关系,所以对代码1和代码2,在多线程环境中,也可以重排。
综上来看,其实happens-before和as-if-serial的本质是一样的,只要保证了这两种关系,执行的顺序则可以重排序,只不过前者针对多线程来说,后者针对单线程来说。