一、首先要说明Java内存模型:参考资料
1、Java为了保证其平台性,使Java应用程序与操作系统内存模型隔离开,需要定义自己的内存模型。在Java内存模型中,内存分为主内存和工作内存两个部分,其中主内存是所有线程所共享的,而工作内存则是每个线程分配一份,各线程的工作内存间彼此独立、互不可见,在线程启动的时候,虚拟机为每个线程分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为了提高执行效率,读取副本比直接读取主内存更快(这里可以简单地将主内存理解为虚拟机中的堆,而工作内存理解为栈(或称为虚拟机栈),栈是连续的小空间、顺序入栈出栈,而堆是不连续的大空间,所以在栈中寻址的速度比堆要快很多)。工作内存与主内存之间的数据交换通过主内存来进行
2、工作内存可以说是主内存的一份缓存,为了避免缓存的不一致性,所以volatile需要废弃此缓存。但除了内存缓存之外,在CPU硬件级别也是有缓存的,即寄存器。假如线程A将变量X由0修改为1的时候,CPU是在其缓存内操作,没有及时回写到内存,那么JVM是无法X=1是能及时被之后执行的线程B看到的,所以JVM在处理volatile变量的时候,也同样用了硬件级别的缓存一致性原则。
二、关于volatile关键字
1、对于一个变量的修改操作,在内存中是这样的,先从变量的地址复制一份出来,修改复制的值,然后把修改后的值写回到该对象原地址。
2、volatile指出 变量是随时可能发生变化的,每次使用它的时候必须从它的地址中读取,编译器生成的汇编代码会重新从变量的地址读取数据。
3、编译器优化做法是,由于编译器发现两次从变量读数据的代码之间的代码没有对变量进行过操作,它会自动把上次读取的数据拿来用。而不是重新从变量的地址读。
4、单例的双重检测
public class ReportController {
private static final String TAG = "ReportController";
private volatile static ReportController mInstance;
private static ActionLogEvent mEvent = new ActionLogEvent();
private ReportController() {
}
public static ReportController init() {
if (mInstance == null) {
synchronized (ReportController.class) {
if (mInstance == null) {
mInstance = new ReportController();
}
}
}
return mInstance;
}
}
双重检测是为了提高效率,如果不同步可能产生多个对象,如果将整个函数同步了那么每次都要同步,其实读是不用同步的,这样写只有在第一需要同步,而在以前这样写也会不对,因为可能重排序是使instance先获得地址,而实例却还没写入地址。后来volatile不准重排序就解决了这个问题。
注:关于重排序,使instance先获得地址,而实例却还没写入地址【 instance要加volatile的原因】由于重排序,会导致2、3调用顺序不一定相同,如果顺序为1-3-2,线程A执行完3未执行2时,线程B切入读取instance,之后A线程执行了2,此时线程A与线程B的工作内存中instance副本包含的内容就不同了。
三、总结
对volatile变量的写,会立即从线程的工作内存写入到主存中,并且会使其他线程工作内存中的变量失效,迫使它重读。而普通变量不会这样。声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步。
1、volatile修饰变量的可见性理解:
volatile的第一条语义是保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A之后的其他线程能看到变量X的变动,本质原因:
2、volatile的第二条语义:禁止指令重排序
1)指令重排序(为了避免CPU内存操作速度远慢于CPU运行速度,导致CPU空置,虚拟机/CPU按一定规则将程序编写顺序打乱)
volatile和synchronized可以禁用重排序
在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。
在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。
2)重排序的部分规则:
a.程序次序规则(Program Order Rule):在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是代码顺序,因为要考虑分支、循环等结构。【 一个线程内,代码块之间是有序的】3、volatile不能保证原子性的理解
Demo:线程A在做了i+1,但未赋值的时候,线程B就开始读取i,那么当线程A赋值i=1,并回写到主内存,而此时线程B已经不再需要i的值了,而是直接交给处理器去做+1的操作,于是当线程B执行完并回写到主内存,i的值仍然是1,而不是预期的2。也就是说,volatile缩短了普通变量在不同线程之间执行的时间差,但仍然存有漏洞,依然不能保证原子性。
注意:只有在对变量读取频率很高的情况下,虚拟机才不会及时回写主内存,而当频率没有达到虚拟机认为的高频率时,普通变量和volatile是同样的处理逻辑。如在每个循环中执行System.out.println(1)加大了读取变量的时间间隔,使虚拟机认为读取频率并不那么高,所以实现了和volatile的效果。volatile的效果在jdk1.2及之前很容易重现,但随着虚拟机的不断优化,如今的普通变量的可见性已经不是那么严重的问题了。