volatile关键字原理
可见性
可见性产生的原因
硬件层面
CPU的执行速度远远大于从主存读取的速度,所以为了尽可能弥补主存的读取这一瓶颈,在CPU和主存之际还有一层高速cache。现在大多都是多核cpu,每个核独有一个cache。
所以当运行在一个核上的一个线程对一个变量进行修改后,其实最先修改的是当前cache里的值,然后cache会将此值同步到主内存,然后再由其他核上的cache同步。由于这种关系,一个线程修改了一个变量,对于其他线程并不是立马可以感知到的。实际上,如果没有触发同步指令,也可能永远感知不到变化。
JMM
Java作为高级语言,屏蔽了CPU cache等底层细节,用 JMM 定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM 抽象了主内存和本地内存的概念。
解决方法
采用这种cache机制,以及jvm在此基础上抽象出的多JMM模型,本质上都是为了提高多线程下CPU执行的效率,但是也是因为这种机制,造成了线程之间的可见性问题。
想要保证线程之间的可见,要么触发同步指令,要么加上volatile关键字,被volatile修饰的内存,只要有修改,立马会同步涉及到的线程。
有序性
CPU中的乱序执行
cpu在底层对代码进行优化,为了提升效率,可能会对代码执行的顺序进行重排序。
代码的执行顺序,这里的代码是具体是指java翻译为汇编后的执行顺序
线程的as-if-serial
在单线程中,从上往下的语句,未必是按照顺序执行。
cpu对单线程的重排序执行,不会影响执行的结果,保证了最终的一致性。
as-if-serial,可以翻译为看上去像是顺序执行,其实也就是代表在单线程中重排序不会对代码结果有影响。但是在多线程中可能会导致线程不安全的问题。
volatile实现可见性
内存屏障
内存屏障是汇编语言中特殊的指令:cpu看到这条指令,前面的必须执行完成,后面的才能执行。
在intel的cpu中,内存屏障的指令是:lfence、sfence、mfence
jvm中的内存屏障
jvm对汇编中的内存屏障有自己的抽象。
所有实现jvm规范的虚拟机,必须实现四个屏障
LoadLoadBarrier,LoadStoreBarrier,StoreLoadBarrier,StoreStoreBarrier
volatile的底层实现
volatile修饰的内存,不可以重排序。
其实质是对volatile修饰变量的读写访问,都不可换顺序。
DLC单例要不要加上volatile
public class SingletonDLC {
private SingletonDLC() {
}
private SingletonDLC instance;
public SingletonDLC getInstance() {
//第一个if是提升性能,如果instance!=null则不需要去获取锁
if (instance == null) {
synchronized (SingletonDLC.class) {
//第二个if是必要的
if (instance == null) {
instance = new SingletonDLC();
}
}
}
return instance;
}
}
对象new的过程
class T{
int m = 8;
}
T t = new T();
对于一个类的new的过程,对应的汇编码为
0 new #2 <T> //在堆内存中分配对象
3 dup
4 invokespecial #3 <T.<init>> //执行构造方法
7 astore_1 //将t执行这块内存
8 return
由汇编码可见,主要分为三步:
1、在堆中分配内存,并且赋予初值,这里的初值是jvm提供的,不是构造方法。m变量在这一步将会赋值为0;
2、执行T的构造方法;
3、将t执行这块内存
如果在单线程中2,3两步的执行顺序不会影响结果,所以cpu可能会对其进行乱序,也就是先将t执行这块区域,然后在执行构造方法。
在单线程中虽然没有影响,但是在多线程中却存在很大问题。
比如线程A在进行new的时候,如果先执行3,则t==null这个条件对于其他线程就不满足类,所以其他线程在执行getInstance的时候会返回这个句柄,但是其实里面的成员值并没有执行构造方法,所以其他线程拿到的其实是一个半初始化状态的对象。