10. 并发终结之Volatile
volatile关键字的作用:保障可见性、保障有序性以及保障long/double类型的变量读写操作的原子性
需要注意的是volatile仅仅只能保障被修饰变量的读、写操作的原子性,且这个赋值的过程不能涉及任何共享变量(比如volatile a++或者volatile a = volatile b + 1)。
注意一点volatile Object o = new Object(),虽然之前说new Object()操作可以分为三步:
1 objRef = allocate(Object.class)分配Object实例所需要的内存空间,并获得一个指向该空间的引用
2 invokeConstructor(objRef)调用Object的构造器初始化objRef所指向的Object实例
3 o = objRef 将实例引用objRef赋值给实例变量o
而volatile仅仅只能保证第3步赋值操作的原子性,但是步骤1,2不涉及共享变量,所以整个赋值操作都可以看做是原子操作,且volatile避免了2,3步骤的重排序,可以解决单例模式双重检测锁DCL的问题。
volatile的可见性
volatile变量的写操作类似释放锁的效果,volatile变量的读操作类似获取锁的效果。
-
volatile写操作,会在该操作之前插入一个释放屏障(Release Barrier),以及操作之后会插入一个存储屏障(Store Barrier)。
Release Barrier禁止该屏障前的任何读、写与屏障后的volatile写操作进行重排序,从而保证了volatile写操作之前的任何读、写都会优先于volatile写被提交,意味着其他读线程看到写线程对volatile写更新的值时,volatile写之前的那些读、写操作对于该读线程也是可见的;
Store Barrier则保证volatile写以该操作前的任何读写的更新同步到主内存,方便其他读写线程的可见性。
image.png
-
volatile读操作,会在该操作之前插入加载屏障(Load Barrier),以及操作之后插入获取屏障(Acquire Barrier)。
Load Barrier通过刷新处理器缓存,从主内存同步共享变量(可能是多个变量)的修改(读线程的Load Barrier和写线程的Store Barrier保障了volatile的写对volatile的读的可见性,只是与锁不同的是,volatile不具备排他性,只能保证获取值的相对新值,即读线程读到共享变量值的那一刻,会丢失别的写线程对该共享变量的更新)。
Acquire Barrier保证了volatile读操作与屏障后面的任何读、写操作的重排序,也就保证了volatile读之后的读、写操作开始之前,写线程对相关共享变量(包括volatile写以及普通写)的更新对当前线程可见。
image.png
public class TestVolatile {
private volatile boolean result = false;
public void writer() {
result = true;
System.out.println("set");
}
public static void main(String[] args) {
TestVolatile test = new TestVolatile();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.writer();
}).start();
while (!test.result) {
}
System.out.println("------");
}
}
======
如果不加volatile就只打印set,不能正常退出
如果加了volatile
set
------
并正常退出
Volatile可以看做是给JIT的一个提示,相当于告诉JIT相应的变量值可能被其他线程修改,从而使JIT不去做一些优化,而避免可见性问题。
此外针对于volatile修饰的数组或者引用类型变量,volatile只能保证读线程能够读到共享变量的相对新值,而不能保证相应对象内部字段(实例变量、类变量)的相对新值。
Volatile与锁相比,是一种更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换或线程调度等操作。但是比普通读写更高,因为处理器缓存失效,每次都要去主内存中读取。