一、volatile作用
①保证内存可见性
通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。
当一个变量被 volatile
修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。
主存可见性的原因:
上图为Java内存模型,由上图可知共享变量是保存在主内存中的,当一个线程需要操作共享变量时,需要从主内存拷贝一份共享变量的副本到本地内存(这个本地内存对其他线程是不可见的),此时如果线程修改了共享变量,则会把修改后的值写回到本地内存,但该变量由本地内存写回主内存的时间是不可控的,只要没有写回主内存后续线程就无法读取到该变量的最新值,这是其一。其二如果其他线程的本地内存中已经包含了该变量的副本,那么即使该线程把最新值写回到主内存,其他线程由于已经有副本了,所以重新去主内存读取该变量的时间也不可控。
另一个版本解释(在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。)
②禁止指令重排序
禁止指令重排序的原因:
JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,程序的执行结果不能改变
double pi = 3.14; //A
double r = 1; //B
double s= pi * r * r;//C
上面的语句,可以按照A->B->C执行,结果为3.14,但是也可以按照B->A->C的顺序执行,因为A、B是两句独立的语句,而C则依赖于A、B,所以A、B可以重排序,但是C却不能排到A、B的前面。JMM保证了重排序不会影响到单线程的执行,但是在多线程中却容易出问题。 比如这样的代码:
int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
假如有两个线程执行上述代码段,线程1先执行write,随后线程2再执行multiply,最后ret的值一定是4吗?结果不一定:
如图所示,write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。 这时候可以为flag加上volatile关键字,禁止重排序,可以确保程序的有序性,也可以上重量级的synchronized和Lock来保证有序性,它们能保证那一块区域里的代码都是一次性执行完毕的。
二、volatile并不能保证原子性
即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。
volatile适用于:对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果准确性的
它是被设计用来修饰不同线程访问和修改的变量。
三、volatile应用
①状态量标记(如上flag)
②懒汉式单例模式
new Singleton():完成3步 分配内存空间1、初始化对象2、将Singleton对象指向分配的内存地址3
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
参考:https://www.jianshu.com/p/0c3a349663db