volatile:
1.保证可见性
2.禁止重排序
我们先来看看一个问题,关于i=i+1的问题。
首先,他不是一个原子性的操作,我们通常将不可拆分的操作称为原子操作
而i=i+1需要先在主存中取得i的值,之后复制到高速缓存之中,CPU再从高速缓存中读取并计算之后存入高速缓存,最后把值再存入内存中。
那么怎么来解决这个问题呢?
有两种方法
第一种是Lock加锁,但是一定会损失并发性,加锁之后其他cpu无法访问
第二种是保证可见性,即当前cpu对这个数据进行更改时,其他数据也可以看到修改。
这个是通过共享变量来判断的,将这个数据设置为共享数据,如果这个数据发生改变,其他持有该数据的cpu都会得到通知,将自己持有的作废之后重新在内存中获取。
上面我们讲了一下关于可见性的问题
那么下面我们来聊一聊关于重排序的问题
什么是重排序呢,在遵守happens--before的原则下,操作系统可以对一些指令进行重排序
比如:
a=1
a++
b=1
我们将第二条和第三条语句交换位置,并不会改变结果
但是有时,看上去互不影响的语句交换顺序是会影响最终结果的
例如:
//线程1
int a=1
int flag = true;
//线程二
if(flag){
xxxxxxxxxxxx
}
如上,我们本来打算是线程1完成所有的初始化操作之后再执行线程二
但是如果重排序,线程一的两条语句重排之后,那么a还没有完成初始化,线程二就会执行。
如果我们用volatile使得禁止指令重排序
我们会在这条语句上添加内存屏障
即保证这条语句前面的语句都在这条语句之前执行
保证这条语句后面的语句都在这条语句之后执行
但是他前面几条语句,以及后面几条语句的执行顺序并不保证
volatile只能保证内存一致性,而不能保证原子性
保证原子性需要synchronized或者lock
public class Main {public volatile int inc = 0;public void increase() {inc++;}public static void main(String[] args) {final Main test = new Main();for(int i=0;i<10;i++){new Thread(){public void run() {for(int j=0;j<1000;j++)test.increase();};}.start();}while(Thread.activeCount()>1) //保证前面的线程都执行完Thread.yield();System.out.println(test.inc);}
}
最终inc的结果一定小于10000
因为inc++并不是原子性操作
我们前面说过,需要先在主存中取得i的值,之后复制到高速缓存之中,CPU再从高速缓存中读取并计算之后存入高速缓存,最后把值再存入内存中。
所有比如现在inc=1
当线程1读取了inc的值,轮到线程二执行,线程二对inc读取并加1,因为线程1只是读取,所以线程二对数据的更改线程一并不知道,线程二将2写入内存,线程一又将读取的数据自增,最后还是2
本来应该是3 ,结果就出现了偏差
所以要保证正确性,就必须保证操作是原子性的
否则使用synchronized或者lock
volatile还有一个经典的使用案例
单例模式中的双重锁
public class Singleton { private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) {// 1 if (instance == null) {// 2 instance = new Singleton();// 3 } } } return instance; }
}