volatile的作用
1、保证变量可见性
说到volatile,就不得不提一个词:“可见性”,可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
要具体理解这句话的含义,还需要看下Java的内存模型:
由于内存和cpu之间的计算速度差距过大,如果cpu直接从内存中读取数据十分影响性能,所以在cpu和内存之间加了一个沟通的桥梁——高速缓存。
Java内存模型规定:
1.所有的变量都存储在主内存中;
2.线程拥有自己的工作内存(即高速缓存),线程的工作内存中保存了该线程所使用到的变量(拷贝自主内存);
3.线程对变量的所有操作都必须在工作内存中进行,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
单线程情况下,不会产生任何问题,但当多线程访问同一变量时,就有可能读到脏数据。
假设有一个成员变量i,初值为0,线程A和线程B同时修改一个变量i,首先,线程A、B将变量i从主存读到工作内存中,初值均为0,线程A将i+10,线程B将i+1,此时,由于线程A可能还没有将工作内存中的数据(i=10)刷新到主存,B没有重新拷贝主存中的数据,导致B线程看到的i仍然为0,也就是说,A线程修改的变量对B线程“不可见”了,即出现“脏读”。
此时,如果对成员变量i加volatile关键字,同样是上述场景,i在每次被线程访问时,都检查变量的地址是否改变,如改变,就强迫从主内存中重读该成员变量的值,并且,当成员变量发生变化时,强迫线程将变化值回写到主内存中,这样在任何时刻,AB线程总是看到某个成员变量的同一个值。
2、禁止指令重排序
1.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;并且在其后面的操作肯定还没有进行。
2.在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
在Java内存模型中,为了保证效率,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
例如我们常见的双重检查单例:
为什么要给instance加上volatile关键字呢?这是由于instance=new Singleton();这句话不是原子操作,在JVM中,这句话被分为三个阶段:
1.为instance分配内存
2.初始化instance
3.将instance变量指向分配的内存空间
由于JVM重排序的存在,上述23两步操作是没有依赖关系的,可能被重排序,也就是说,在instance还没被初始化的时候,instance就已经不为null了,这时,另一个线程执行到第一个if,判断不为null,直接return instance,导致异常。
volatile的实现原理
1、可见性
对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据回写到系统内存, 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。
但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。
2、有序性
Lock前缀指令实际上相当于一个内存屏障,它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
volatile的使用条件
1.对变量的写操作不依赖于当前值;
2.该变量没有包含在具有其他变量的不变式中。
volatile的注意点
上面一条讲到了volatile关键字的使用条件,从注意点的角度来解释一下。
首先,volatile虽然可以用在并发编程中,但是不能代替synchronized关键字,因为volatile关键字只能保证并发编程中三大概念中的两个,即可见性和有序性,但是它不能保证程序的原子性,举例说明:
该例中,inc的结果为10000吗?不是,那问题出在哪里?
问题在于,increase方法中,inc++这个操作不具备原子性,线程要先读到位于主内存中的inc的值,然后对其进行+1,然后再将其放回主内存中,在这三步中,假设线程A读取了inc,正在进行+1操作,此时,线程B读取inc,由于线程A没有操作完,所有并没有将+1后的操作写入主存,导致B读取的仍为旧值,最终导致程序的结果小于预期结果,解决方法:可以将increase方法加上synchronized关键字,保证其原子性。
总结
本篇大致讲解了volatile的用途以及实现原理,上文也说到,他不能代替synchronized关键字,在下篇文章中,就要讲到Java中的各种锁,看下Java的锁机制是如何保证效率的情况下,在多线程中安全运行的。