Volatile
JMM模型
指Java内存模型(Java Memory Model,JMM),不是内存布局,不是指所谓的堆、栈、方法区,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。。
每个Java线程都有自己的工作内存。操作共享数据,首先将数据从主内存拷贝到线程自己的工作内存中,得到一份拷贝,操作完毕后在写会主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
对volatile的理解
volatile 是 Java虚拟机提供的轻量级同步机制
- 保证可见性(一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量这种修改(变化))
解决缓存一致性方案有两种: 1. 通过在总线加LOCK#锁的方式; 2. 通过缓存一致性协议。 但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。 第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。 所以JMM就解决这个问题。 有volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。将当前处理器缓存行数据刷写到系统主内存。 这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。这样就保证了多个处理器的缓存是一致的,对应的处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设置无效状态,当处理器对这个数据进行修改操作的时候会重新从主内存中把数据读取到缓存里。
- 不保证原子性
- 禁止指令重排
指令重排:
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种
编译器优化的重排
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令并行的重排
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序
内存系统的重排
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题
先了解一个概念,内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行顺序
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重新排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
DEMO:
1 | import java.util.concurrent.TimeUnit; |
##多线程下单例模式的安全问题
###DCL(Double Check Lock)模式
1 | public class SingletonDemo { |
instance=new SingletonDemo();可以大致分为三步
1 | memory = allocate(); //1.分配内存 |
其中2、3没有数据依赖关系,可能发生重排。如果发生,此时内存已经分配,那么instance=memory不为null。如果此时线程挂起,instance(memory)还未执行,对象还未初始化。由于instance!=null,所以两次判断都跳过,最后返回的instance没有任何内容,还没初始化。
解决的方法就是对singletondemo对象添加上volatile关键字,禁止指令重排。