volatile 浅析

概念

volatile原语保障了多线程下变量的原子性、可见性、有序性。

原子性

在32位虚拟机下对longdouble类型的赋值操作会拆分位高位、低位两步完成修改,而volatile可使用内存屏障来达到原子性,保证不存在中间值,但是由于读取的一刻其他线程也有可能改变值,所以复合操作无法达到原子性。

64位下不存在该问题,并且近代jdk中普通操作也能达到原子性

可见性

其目的是使多线程环境下对共享变量的修改能被其他线程立即查看到。

由于线程对变量的读取是先从线程的工作内存(cpu缓存)中获取,不存在才从主内存中获取。

那么在多线程环境下多普通共享变量的修改操作就会由于cpu缓存中已存在而导致的数据不一致。

为了解决这个问题,处理器会对编译后指令增加lock指令前缀,大部分处理器架构采用了RingBus + MESI协议的方式来解决,部分老版本cpu架构则采用锁总线来达到效果。

原理是在修改数据时候更新cpu缓存之后立即写回主存、并且通知到使用相同变量线程,将该变量设置为无效,当读取变量时再从主存或者寄存器中获取。

由于volatile的可见性,也可用来完成轻量锁的实现,例如线程的终止判断

有序性

操作系统为了使cpu流水线的各个阶段不存在空闲内核,往往会多当前编译好的指令进行重排序,但是只会保证单条线程的程序正确性,并不保障并发环境下的正确性。

JVM为了保证volatile域的可见性(happens-before),会在编译时对指令前后都加入内存屏障指令lock前缀,使得不会将后面的指令重排序到内存屏障之前的位置。

  1. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

最经典的场景有double check单例,由于重排序,可能出现对象尚未创建成功,但是对象引用缺被赋值使用,造成空指针异常。