上篇文章说过,在双重检查的单例模式中,如果不用volatile,可能会导致重排序,进而产生一个空的单例。而volatile可以有效避免重排序,接着来探究下这一实现的原理吧
可以认为hasppens before是JMM为java程序员设定的程序执行前后顺序的规则,注意,只是程序员可以这么去看程序的执行顺序,即语义上的happens before,实际上底层执行可能还是存在着为了优化而进行的重排序,不过没关系啦,我们只要知道语义正确就好啦。
对一个volatile变量的写happens before后续对该变量的读,即不能重排序这两者
对一个锁的解锁总是hasppens before随后对该锁的加锁
一个线程中的每个操作,happens before 线程内任意后续操作
说内存语义前,先了解volatile的两个性质
可见性:对该变量的读,总是能看到读之前任意一个线程对这个变量最后的写入,其实就是happens-before规则换了个说法
原子性:对volatile的读写是原子性的,volatile具有锁语义保证原子性 ,但是也只是读这单个操作原子,对于++这种复合操作并不具备。volatile是对单个volatile变量的读写具有原子语义,synchronized是对代码块具有原子性
JSR-133后,volatile的写/读就具有了锁的内存语义
**volatile写的内存语义:**写一个volatile变量时,会把该线程对应本地内存中的共享变量刷新到主内存,对应的释放锁的内存语义
**volatile读的内存语义:**一个volatile变量时,JMM会把线程对应本地内存置为无效,线程接下来从主内存读取共享变量,对应的获取锁的内存语义
是否能重排序 | 第二个操作 | 第二个操作 | 第二个操作 |
---|---|---|---|
第一个操作 | 普通读/写 | volatile 读 | volatile 写 |
普通读/写 | NO | ||
volatile 读 | NO | NO | NO |
volatile 写 | NO | NO |
即volatile写不能与之前的任何操作重排序,确保volatile写能将写之前所有操作结果刷新到主内存;volatile读不能与volatile之后任意操作结果重排序,确保读到的是之前所有操作的结果。同时volatile写和volatile读不能重排序。
进一步的,volatile禁止重排序可以由如下内存屏障的插入来实现。首先来了解下内存屏障吧
现在大多数处理器都允许读写重排序同时写操作会先写入写缓冲区最后才刷新到内存,这就会造成一个问题
初始状态a=b=0 | 处理器A | 处理器B |
---|---|---|
代码 | a=1;x=b; | b=2;y=a; |
运行结果 | x=0 | y=0 |
处理器A和B并行同时执行,上述结果就是一个读写重排序的体现,先去读,此时写入的值还没刷到内存呢,同时写缓冲区仅对自己处理器可见,造成内存不可见问题,自然就是最后的0。JMM为了保证内存可见性,会在适当位置插入内存屏障指令来禁止特定类型重排序。
分别是以下四类,
volatile写之前可以插入storestore,确保之前所有操作都刷新到主内存;volatile写之后可以插入storeload,确保volatile完成才能读。
volatile读之后插入loadload和loadstore,即禁止volatile读之后的任意操作和volatile读重排序。
看到这里,我想我们应该明白了为什么volatile可以保证单例模式中不被重排序了。回过头去看下
1 memory=allocate(); // 分配对象内存空间 2 ctorInstance(memory); // 初始化对象 3 singleton=memory; // 将singleton指向刚分配好的空间 复制代码
这是正常的,我们说不加volatile可能会重排序如下:
1 memory=allocate(); // 分配对象内存空间 2 singleton=memory; // 将singleton指向刚分配好的空间 3 ctorInstance(memory); // 初始化对象 复制代码
我们如果使用volatile修饰instance变量的话,singleton写之前会加storestore,所以2和3就不能重排序,所以保证了之前那种错误不会再发生