volatile关键字的作用
volatile用于修饰变量
- 保证变量的内存可见性
- 不保证原子性
- 禁止指令重排序
JMM模型
Java内存模型(Java Memory Model)是一种抽象的,本身不存在的一组规范,来定制程序中各个变量的访问方式.
JMM关于同步的规定
- 线程解锁前,必须把共享变量的值写回主内存.
- 线程加锁前,必须读取主内存中变量的最新值到自己工作内存.
- 加锁与解锁必须使用同一把锁.
由于JVM运行程序的载体是线程,而JVM创建每个线程时都会为该线程分配一块私有的工作内存(也称为栈内存).JMM规定所有变量都存储在主内存中,主内存是共享区域,所有线程都可以访问.但线程对变量的操作(读取赋值等)都必须在工作内存中进行,首先从主内存中读取变量到工作内存中,然后对该变量进行操作,操作完成把该变量写回主内存.各个线程的工作内存中存储着主内存变量的拷贝副本.不同线程无法访问对方的工作内存,因此必须通过主内存来完成.
可见性
不同线程各自的内存空间里的变量是不能互相访问的,称为不可见性.
如果A线程修改了从主内存拷贝的一个变量,还未写回主内存,而B线程从主内存中读取了该变量,那么此变量的值就是A线程修改前的值,这就造成了AB两个线程的不可见问题.
原子性
原子性表示某个操作是不可拆分的.
比如i++操作,在底层被拆分成了三个操作:执行getfiled拿到原始的i,执行iadd进行+1操作,执行putfield操作把累加后的值写回.
如果有变量i初始值为0,线程A在工作内存中执行i++操作,首先拿到原始i,但还未执行+1操作时,线程B得到了cpu时间,线程A阻塞了.线程B在工作内存中也执行了i++操作,然后把i的值1写回了主内存.但是此时线程A不会再返回去读取主内存的i值,此时A线程工作内存中的i值还是0,A线程继续执行i++操作,然后把i的值1写回到主内存.此时主内存的i值就是1,B线程的操作就被A线程覆盖了.
有序性
为了尽可能的减少内存速度远慢于cpu速度所带来的cou闲置问题,虚拟机会按照自己的特定规则,将程序的编写顺序打乱执行,以尽可能的充分利用cpu,提高执行效率.这个前提是乱序执行不会导致程序结果出错.在单线程环境下,可以保证程序结果正确,但是在多线程环境下,重排序后的程序结果正确性是无法保证的.
volatile的实现原理
内存屏障(Memory Barrier),是一个cpu指令,作用有两个:
- 保证特定操作的执行顺序.
- 保证某些变量的内存可见性.
编译器和处理器都可以执行指令重排序优化,如果在指令间插入一条内存屏障则会告诉编译器和cpu,不管什么指令都不能和这条内存屏障进行指令重排序,也就是通过插入内存屏障禁止在内存屏障的前后进行指令重排序.内存屏障另一个作用就是强制刷新出cpu的各种缓存数据,因此cpu上的所有线程都能读取到这些数据的最新值.
volatile的使用场景
- 单例模式DCL(双重检查锁)
class SingletonDemo2 {
public volatile static SingletonDemo2 instance = null;
private SingletonDemo2() {
}
public static SingletonDemo2 getInstance() {
if (null == instance) {
synchronized (SingletonDemo2.class) {
if (null == instance) {
instance = new SingletonDemo2();
}
}
}
return instance;
}
}
单例对象必须加volatile关键字的原因:
DCL不一定线程安全,因为有指令重排序的存在,加入volatile可以禁止指令重排序.初始化一个单例对象底层可分为以下步骤:
- 分配内存空间
- 初始化对象
- 设置对象指向刚分配的内存地址
步骤二和步骤三不存在数据依赖关系,而且指令重排后在单线程情况下不会改变程序的结果,因此这种重排优化是允许的.重排后会导致A线程拿到的instance不为null,但是此时instance未必已经完成初始化,也就造成了线程安全问题.