了解volatile、内存与happens-before规则

大家都知道 , 在阿里巴巴泰山版开发手册中有这一段 , 在并发情况下使用延迟初始化的方法实现单例模式时 , 需要将目标属性声明为volatile 。
了解volatile、内存与happens-before规则文章插图
volatile关键字在 Java 中的作用是保证变量的可见性和防止指令重排 。
一、保证变量的可见性在知道volatile是如何保证变量的可见性之前 , 我们先要知道内存不可见的两个原因:
1、CPU的运行速度是远远高于内存的读写速度的 , 为了不让CPU等待读写内存数据 , 现代CPU和内存之间都存在一个高速缓存cache(实际上是一个多级寄存器) , 如下图:
了解volatile、内存与happens-before规则文章插图
线程在运行的过程中会把主内存的数据拷贝一份到线程内部cache中 , 其实就是访问自己的内部cache 。 如果线程B把数据加载进内部缓存cache中 , 线程A再修改了数据 。 即使重新写入主内存 , 但是线程B不会重新从主内存加载变量 , 看到的还是自己cache中的变量 , 所以线程B是读取不到线程A更新后的值 。
了解volatile、内存与happens-before规则文章插图
在多处理器下 , 为了保证各个处理器的缓存是一致的 , 就会实现缓存一致性协议 , 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了 , 当处理器发现自己缓存行对应的内存地址被修改 , 就会将当前处理器的缓存行设置成无效状态 , 当处理器对这个数据进行修改操作的时候 , 会重新从系统内存中把数据读到处理器缓存里 。 volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值 。但是 , 我们也都知道volatile只能保证可见性 , 不能保证原子性 。 多个线程同时读取这个共享变量的值 , 就算保证其他线程修改的可见性 , 也不能保证线程之间读取到同样的值然后相互覆盖对方的值的情况 。
二、防止指令重排我们再来看指令重排 。
1、定义指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序 。
介绍指令重排之前 , 首先介绍一下内存交互操作的8种指令吧 。 虚拟机实现必须保证每一个操作都是原子的 , 不可再分的(对于double和long类型的变量来说 , load、store、read和write操作在某些平台上允许例外)
了解volatile、内存与happens-before规则文章插图
如图所示:
了解volatile、内存与happens-before规则文章插图
既然操作可以被分解为很多步骤, 那么多条操作指令就不一定依次序执行 , 因为每次只执行一条指令, 依次执行效率太低了 。 就像小时候学习的煮饭烧水任务时间分配一样 , 内存也会很聪明的分配时间 。
本来想给大家整一个指令重排序的例子的 , 但是不管是我自己写还是用别人的代码 , 我的电脑都没办法让它重排序 。 但是我们都知道 , 指令重排是确实存在的(CPU确实会进行重排序 , 但是这种重排序是无法被我们观测到和控制的) 。
一般重排序可以分为如下三种:
了解volatile、内存与happens-before规则文章插图

  • 1、编译器优化的重排序 。 编译器在不改变单线程程序语义的前提下 , 可以重新安排语句的执行顺序;
  • 2、指令级并行的重排序 。 现代处理器采用了指令级并行技术来将多条指令重叠执行 。 如果不存在数据依赖性 , 处理器可以改变语句对应机器指令的执行顺序;
  • 3、内存系统的重排序 。 由于处理器使用缓存和读/写缓冲区 , 这使得加载和存储操作看上去可能是在乱序执行的 。
2、原理我们来看加了volatile前后的代码,用的就是阿里规约提供给我们的双重检查锁的代码 。 我们分别编译了两次 , 第一个是没有使用volatile关键字修饰的 , 第二个是使用volatile关键字来修饰 , 然后取出他们的的汇编代码(实在是设计的地方太底层 , 其实这里算是用到了策略模式了)
未使用volatile修饰
0x000000010d29e93b: mov%rax,%r100x000000010d29e93e: shr$0x3,%r100x000000010d29e942: mov%r10d,0x68(%rsi)0x000000010d29e946: shr$0x9,%rsi0x000000010d29e94a: movabs $0xfe403000,%rax0x000000010d29e954: movb$0x0,(%rsi,%rax,1)复制代码使用volatile修饰
0x0000000114353959: mov%rax,%r100x000000011435395c: shr$0x3,%r100x0000000114353960: mov%r10d,0x68(%rsi)0x0000000114353964: shr$0x9,%rsi0x0000000114353968: movabs $0x10db6e000,%rax0x0000000114353972: movb$0x0,(%rsi,%rax,1)0x0000000114353976: lock addl $0x0,(%rsp)复制代码