volatile是一种轻量级的同步机制,不会引起的线程上下文切换,可以保证可见性和禁止重排序,但不能保证原子性带来的线程安全问题。
volatile特性
可见性
当有多个线程访问同一个变量时,一个线程对此变量的修改,其他线程应该立刻可获取到修改后的值。
一个变量用volatile修饰后,当对此变量写入时,会立刻将写入后的值刷新到主存中,并且将其他线程本地内存中保存的值失效,其他线程读取此变量时,会直接从主内存中读取。
public class VolatileDemo {
private static boolean stop = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while(stop){//无限循环
}
}).start();
//确保上面线程先执行
Thread.sleep(100);
//虽然修改为false,但是上面线程依旧不会停
stop = false;
System.out.println("main thread end...");
}
}
这就是可见性问题,只需要加上volatile修饰符即可。
private volatile static boolean stop = true;
指令重排序
编译器有时会为了效率,在保证不影响最终结果的前提下,会改变代码的执行顺序。
在单线程下,不会存在问题,但是在多线程下就会带来一些问题,比如单例模式中,双重锁校验,锁的对象必须用volatile修改。
单例模式:双重锁校验
public class DoubleCheck {
private static volatile DoubleCheck doubleCheck = null;
private DoubleCheck() {
}
public static DoubleCheck getInstance() {
//第一次校验
if (doubleCheck == null) {
synchronized (DoubleCheck.class) {
//第二次校验
if (doubleCheck == null) {
doubleCheck = new DoubleCheck();
}
}
}
return doubleCheck;
}
}
doubleCheck = new DoubleCheck();可以分解为3行伪代码
1、memory = allocate() //分配内存
2、ctorInstanc(memory) //初始化对象
3、doubleCheck = memory //设置doubleCheck指向刚分配的地址
如果多线程下,由于重排序顺序变为1-3-2,那么使用者就有可能得到一个为null的对象。
volatile重排序规则表
1、当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
2、当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
3、当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
volatile的内存屏障
volatile写
storestore和storeload保证了,前后语句顺序不会被重排序。
store1 :storestore :store2,保证store1一定在store2之前执行。
store1 :storeload:load2,保证store1一定在load2之前执行。
volatile读
load1:loadload:load2,保证load1一定在load2之前执行。
load1 :loadstore:store2, 保证load1一定在store2之前执行。
内存屏障会提供3个功能:
1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2、它会强制将对缓存的修改操作立即写入主存;
3、如果是写操作,它会导致其他CPU中对应的缓存行无效。