volatile 在多线程并发中扮演重要的角色,比synchronized的使用和执行成本更低,因为它不会引起线程上下文切换和调度。
被volatile标记的变量为共享变量
- 保证了不同线程对该变量操作的内存可见性;
- 禁止指令重排序
一、volatile定义
volatile在java语言规范的描述(jsl8):
翻译:
Java编程语言允许线程访问共享变量。作为一个规则,为了确保共享变量被一致和可靠地更新,线程应该通过获得一个锁来确保它独家使用这些变量,传统上,该锁强制对这些共享变量进行相互排除。Java编程语言提供了第二种机制,即 volatile 字段,它在某些目的上比锁定更方便。字段可以声明为volatile,在这种情况下,Java内存模型确保所有线程看到变量的一致值。如果一个 final 变量也被声明为 volatile,那么这是一个编译时错误。
在jvm规范里看到:(规范表述模糊)
二、原理
保证可见性原理:
instance = new Singleton();
转换为汇编代码如下(instance 是volatile 变量):
0x01a3de1d: movb $0×0,0×11048(%esi);
0x01a3de24: lock add1 $0×0,(%esp);
volatile变量进行写操作时会多出第二行汇编代码:带Lock的前缀指令
这带Lock的前缀指令在多核处理器会产生两件事:
- 将当前处理器缓存行的数据写会内存。
- 这个写回内存的操作会使其他处理器的缓存了该缓存地址的数据无效。
来看看这两件事情是如何实现的:
对于(1):早期LOCK #信号是采取锁总线的方式,但由于开销过大,
而后采用锁缓存的方式。锁定这块内存区域的缓存并将其回写到内存,且采用缓存一致性机制确保修改的原子性。此操作成为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存数据。
缓存一致性协议:在多处理器的情况下,每个处理器总是嗅探总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己对应的缓存对应的地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作的时候,会重新从系统中把数据督导处理器的缓存里。这个协议被称之为缓存一致性协议。
缓存一致性协议有多种,有MSI、MESI、MESOI,总体的思路是一样的,不同的是后面的协议通过增加了某些状态,从而在某些场景能进一步减少通过总线与主存打交道的操作,其中比较出名的是MESI。
对于(2):部分处理器使用MESI控制协议去维护缓存一致性。处理器使用嗅探技术保证内部缓存、其他处理器缓存、内存数据在总线上保证一致。例如:一个处理器通过嗅探检测到其他处理器打算写一个处于共享的内存数据,那么正在嗅探的处理会把它的缓存行设置无效,下次访问此数据时强制执行缓存填充。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
如下图所示:
禁止指令重排原理:插入内存屏障
三、优化
方式:
在JDK7里新增一个队列集合类 LinkedTransferQueue
它在使用 volatile变量时,使用追加字节的方式优化出入队的性能。
其有一个内部类PaddedAtomicReference将volatile变量追加到64字节。
原理:
对于大部分处理器的高速缓存行是64个字节,不支持部分填充缓存行。
意味着如果头结点和尾结点都不足64字节的话,处理器会将它们都读到同一个高速缓存行,锁定缓存行时则会同时锁定头尾节点,会影响到其他处理器的操作,不然头尾节点可以同时操作。追加字节就是避免二者加载到同一缓存行。