volatile关键字
第一篇就决定谈谈关于volatile(改博客期间突然想到之前关于此问题的回答,觉得不是很全面,顺便稍加补充)
原回答
多线程操作三个基本特性—原子性、可见性、有序性
volatile可以保证其中的可见性、有序性,但不能保证原子性(锁)
可见性
提到可见性必须了解java内存模型(JMM),即线程间共享内存和线程独占内存的缓存一致性问题
如下图,未添加volatile关键字的共享变量n,假设线程1先运行,线程1在进行++n操作后将n的值更新为1,线程2在进行++n操作时应该得到的初始值为n=1,但是由于没有添加volatile关键字,导致线程2在获取n的值时,线程1的更新并未刷写到主内存中,于是两个线程运行完的结果n=1并非为预期值。
保证线程对变量或对象的改变可以立刻刷写到主内存中
有序性
指令重排:JVM指令重排和CPU指令重排
一般JVM指令重排不会导致程序运行出现错误,但是cpu不会考虑语言特性,无法识别java的多线程操作,为了cpu提升性能进行重排的指令可能会导致程序崩溃
volatile作为标识,在运行过程中调用本地方法栈的native方法操作cpu,禁止cpu指令重排
补充
如果我们将变量声明为
volatile
,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序
在 Java 中,Unsafe
类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:
1 | public native void loadFence(); |
双重校验锁实现对象单例(线程安全)
1 | public class Singleton { |
两次判空
第一次,所有调用该方法的线程同事校验uniqueInstance是否为null,为null的进入判断,但是只有一个线程可以拿到锁,其他线程阻塞等待
第二次,假设第一个未初始化对象初始化完成释放锁,这时uniqueInstance已创建实例,但是阻塞等待的线程已经通过第一次判空检测,所以拿到锁之后需要进行第二次判空,判断是否之前有线程已经初始化uniqueInstance,因为需要在第一次初始化成功后立刻刷新uniqueInstance实例信息到主内存,所以必须用volatile修饰