volatile
关键字是Java提供的一种轻量级同步机制。它能够保证可见性和有序性,但是不能保证原子性。
可见性
每个Java线程都有自己的工作内存。操作数据,首先从主内存中读取数据,得到一份拷贝,操作完毕后再写回到主内存。 这样某个线程对主内存内容的更改,立刻通知到其它线程。
可见性测试:
class MyData{
//保证多个线程对number的修改其他线程可见
volatile int number = 0;
public void add60(){
this.number = 60;
}
public void increNum(){
number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo {
public static void main(String[] args) {
volatileVisibility();
}
//可见性
public static void volatileVisibility() {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t come in");
//暂停3秒
try{
TimeUnit.SECONDS.sleep(3);
}catch (Exception e){
e.printStackTrace();
}
myData.add60();
System.out.println(Thread.currentThread().getName()+"\t update number value:"+myData.number);
},"A").start();
while (myData.number==0){
//当没有使用 volatile时,main线程就一直在这里等待,没有感知到number更改为60
}
System.out.println(Thread.currentThread().getName()+"\t mission is over main get number:"+myData.number);
}
}
一开始number变量没有用volatile修饰,所以程序运行的结果是:
AAA come in
AAA update number value: 60
虽然一个线程把number修改成了60,但是main线程持有的仍然是最开始的0,所以一直循环,程序不会结束。
如果对number添加了volatile修饰,运行结果是:
AAA come in
AAA update number value: 60
main mission is over. main get number value: 60
可见某个线程对number的修改,会立刻反映到主内存上。
原子性
volatile 并不能保证操作的原子性。比如一条number++的操作,会形成3条指令。
getfield //读
iconst_1 //常量1
iadd //加1操作
putfield //写回主存
假设有3个线程,分别执行number++,都先从主内存中拿到 number=0,然后三个线程分别进行操作。假设线程0还没有将number=1,写回主存。但是此时线程1、2已经拿到了number=0,在这个时间间隙,发生了写覆盖,线程1、2将number变成1。
在这个过程中 number++ 作为一个原子操作应当不被打断,因此 volatile 不保证原子性。
有序性
volatile 可以保证有序性,也就是防止指令重排序。所谓指令重排序,就是出于优化考虑,我们写的程序代码被翻译成一条条指令,CPU执行指令的顺序可能跟程序员自己编写的顺序不一致。
int x = 11; //语句1
int y = 12; //语句2
x = x + 5; //语句3
y = x * x; //语句4
以上例子,可能出现的执行顺序有1234、2134、1342,这三个都没有问题,最终结果都是x = 16,y=256。但是如果是4开头,就有问题了,y=0。这个时候就不需要指令重排序。
单线程情况不存在指令重排,多线程环境线程交替执行,编译器优化重排的存在,两个线程中使用的变量能否保证一致性无法确定。
volatile底层是用CPU的内存屏障(Memory Barrier)指令来实现禁止指令重排,有两个作用,一个是保证特定操作的顺序性,二是保证变量的可见性。在指令之间插入一条Memory Barrier指令,告诉编译器和CPU,在Memory Barrier指令之间的指令不能被重排序。通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。
volatile 在单例模式的使用
public class Singleton {
private static volatile Singleton instance=null;
private SingletonDemo(){}
//DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
public static Singleton getInstance(){
if (instance==null){
synchronized (Singleton.class){
if (instance==null){
instance=new Singleton();
}
}
}
return instance;
}
虽然使用双重检索保证了多线程获取的实例是单例的,但是不使用 volatile 修饰 instance 会出现指令重排的现象,instance=new Singleton(); 在编译阶段分为三步:
memory = allocate(); //1.分配内存
instance(memory); //2.初始化对象
instance = memory; //3.设置引用地址
如果2,3操作被重排,将会导致,跳过初始化对象这步,最终返回的 instance 未进行初始化。