Java并发关键字解析-volatile

372 阅读5分钟

volatile简介

被volatile修饰的变量具备以下特性

  • 保证变量对所有线程的可见性
  • 禁止指令重排序
  • 不保证原子性

volatile保证可见性

在生成汇编代码时,volatile修饰的变量在进行写操作时会多出Lock前缀的指令,Lock前缀指令相当于内存屏障,主要有两方面的影响:

  • 将当前处理器缓存行的数据写回系统内存。
  • 这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。 为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的的数据读到内部缓存再进行操作,但是操作完之后不知何时写回内存。 如果对声明了volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写进系统内存。 但是就算是写回内存,如果其他处理器缓存仍然是旧的,再执行计算操作就会有问题。所以在多处理器下,要保证缓存一致,就要实现缓存一致性协议。 每个处理器都会通过嗅探在总线上传播的数据来检查自己缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存设置为无效状态,当处理器对这个数据修改操作时,会重新从系统内存中把数据读到处理器缓存里。

volatile保证有序性

为了提高系统性能,编译器和处理器通常会对指令进行指令重排序。

  • 编译器优化重排
  • 指令级的并行重排
  • 内存系统的重排
  1. java内存模型(JMM)通过Happens-Before原则具备有序性,其中有一条为volatile变量规则,对一个volatile域的写,happens-before于对这个volatile域的读。
  2. volatile是通过内存屏障实现的,保证按照特定顺序执行和某些变量的有序性。 有如下四种内存屏障:
屏障类型简称指令示例说明
LoadLoad Barries读-读 屏障Load1;LoadLoad; Load2(Load1代表加载数据,Store1表示刷新数据到内存)确保Load1数据的状态先于Load2及所有后续装载指令的装载。
StoreStore Barries写-写 屏障Store1;StoreStore; Store2确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储。
LoadStore Barries读-写 屏障Load1;StoreStore; Store2确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存。
StoreLoad Barries写-读 屏障Store1;StoreLoad; Load2确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后才执行该屏障之后的内存访问指令。

volatile写的场景如何插入内存屏障:

  • 在每个volatile写操作的前面插入一个StoreStore屏障(写-写 屏障)。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障(写-读 屏障)。

image.png volatile读场景如何插入内存屏障:

  • 在每个volatile读操作的后面插入一个LoadLoad屏障(读-读 屏障)。
  • 在每个volatile读操作的后面插入一个LoadStore屏障(读-写 屏障)。

image.png

volatile使用实例

单例模式的实现 `class Singleton{ private volatile static Singleton instance = null;

private Singleton() {

}
public static Singleton getInstance() {
    if(instance==null) {//①
        synchronized (Singleton.class) {
            if(instance==null)
                instance = new Singleton();//②
        }
    }
    return instance;
}

为什么需要volatile?

syschronized不具备禁止指令重排的特性 instance = new Singleton();会有以下三个步骤

  1. Java虚拟机为对象分配一块内存x
  2. 在内存x上为对象进行初始化
  3. 将内存x的地址赋给instance变量 如果无volatile变量修饰,经过指令重排会出现以下情况:
  4. Java虚拟机为对象分配一块内存x
  5. 将内存x的地址赋给instance变量
  6. 在内存x上为对象进行初始化

有两个线程执行getInstance()方法,假如线程A进入代码的注释中的第②处,并执行到了重排指令的(2),与其同时线程B刚好代码注释中的第①处的if判断。此时,instance有线程A把内存地址x地址赋值给了instance,那么instance已经不为空只是没有初始化完成,线程B就返回了一个没有完成初始化的instance,最终使用时候会出现空指针的错误。

volatile不保证原子性为什么还要使用?

volatile是轻量级的同步机制,对性能的影响比synchronized小。

典型的用法:检查某个状态标记以判断是否退出循环。

那为什么我们不直接用synchorized,lock锁?它们既可以保证可见性,又可以保证原子性为何不用呢?

因为synchorized和lock是排他锁(悲观锁),如果有多个线程需要访问这个变量,将会发生竞争,只有一个线程可以访问这个变量,其他线程被阻塞了,会影响程序的性能。

参考:反制面试官 | 14张原理图 | 再也不怕被问 volatile! - 掘金 (juejin.cn)