volatile关键字
1.volatile是什么?
volatile是java虚拟机提供的轻量级的同步机制,具有如下特性:
a.保证可见性
b.不保证原子性
c.禁止指令重排
2.JMM内存模型?
JMM——https://www.cnblogs.com/zhouxuezheng/p/14950302.html
3.volatile的可见性代码验证:
主线程中判断资源类Cache中的变量a的值是否为0,如果为0则一直死循环。新开的线程A会在2秒钟后去改变Cache中a的值,但是主线程不能及时获取到a的最新值从而一直死循环,程序并不会在2秒钟结束。即线程A中对Cache中a的修改对main线程而言不可见 。如果在变量a上添加volatile关键字,程序就可在2秒后就可正常退出。
注意:如果在while代码块中有System.out.println();程序也会在2秒后正常退出,这是因为System.out.println()方法底层有synchronized关键字,这会导致main线程强制获取主存中的数据。
class Cache{ int a = 0; public void setA() { a = 2; } } public class VolatileDemo { public static void main(String[] args) { Cache cache = new Cache(); new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) {} cache.setA(); }, "线程A").start(); while (cache.a == 0) { } } }
4.volatile无法保证原子性
如果能保证原子性,则输出的结果应该恒等于20000,然而结果不等于20000,会存在写覆盖的问题
class Cache{ volatile int a = 0; public void setA() { a++; } } public class VolatileDemo { public static void main(String[] args) { Cache cache = new Cache(); for (int i = 0; i < 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { cache.setA(); } }).start(); } try { Thread.sleep(2000); } catch (InterruptedException e) {} System.out.println(cache.a); } }
运行结果:
5.如何解决原子性问题呢?
使用AtomicInteger原子整型类(底层CAS)
6.volatile禁止指令重排理解?
在多线程环境交替执行set与read,语句1与语句2不存在数据依赖性,由于编译器和CPU优化重排的存在,会导致read中变量一致性无法确定,输出结果无法预测。volatile底层就是在写操作后添加一个store屏障指令,将工作内存中的共享变量刷新回到主内存,在读操作前添加一个load屏障指令,从主存中读取共享变量
class Cache{ int a = 0; boolean flag = false; public void set(){ a = 1; // 语句1 flag = true; // 语句2 } public void read(){ if (flag) { a = a + 1; System.out.println(a); } } }
7.你在哪些地方用到过volatile?
单例模式DCL(double-checked locking)代码
public class SingletonDemo { private static SingletonDemo instance = null; private SingletonDemo() { } public static SingletonDemo getInstance(){ if (instance == null) { synchronized (SingletonDemo.class) { if (instance == null) { instance = new SingletonDemo(); } } } return instance; } }
8.单例模式volatile分析
DCL机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。
原因在于:某个线程执行到第一次检测,读取到的instance不为null,instance的引用对象可能没有完成初始化。
instance = new SingletonDemo();可以分成以下3步完成(伪代码):
memory = allcote(); // 1.分配对象内存空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚刚分配的内存地址,此时instance != null
步骤2与步骤3不存在数据依赖关系,重排后执行结果在单线程中并没有改变,因此这种重排优化是被允许的
memory = allcote(); // 1.分配对象内存空间
instance = memory; // 3.设置instance指向刚刚分配的内存地址,此时instance != null,此时对象还未初始化
instance(memory); // 2.初始化对象
指令重排只会保证串行语义的执行一致性(单线程),但并不关心多线程间的语义一致性
所以当一条线程方位到instance不为null时,instance的实例不一定初始化完成,也就造成了线程不安全的问题