Java volatile关键字

volatile: 易变的,不稳定的

因为声明了易变,告诉线程每次都拉取内存原始内容,保证了顺序和可见性

  • 可见性:变量的修改对其他线程立即可见
  • 有序性:操作系统和JVM可能对指令重排序,结果正确性不受重排序影响

两个线程在无任何同步措施时,单一线程内操作可以重排序,不同线程间内存写回时机也不同,所以整体上是无序的,不可预测
处理器环境下可以通过总线机制保证多核对内存访问是以串行方式执行

Volatile作用


volatile写:把本地副本刷新到主内存
volatile读:把本地副本无效化,从主内存加载
同一volatile变量上多个操作不会重排序

使用事项

  • long double高低各32位,32位环境下多线程下加volatile可以保证原子性读写
  • static变量类共享,但是多线程访问下还是会有副本机制,也需要volatile修饰,常用于一写多读情况下控制状态标志,写线程改变了状态,其他读线程能够及时发现
  • volatile修饰数组只能保护指向数组的引用,不能保护数组内容
  • volatile不是锁,也不能保证操作的原子性(比如n++),不能替代synchronized

优化产生问题


可见性和有序性都是系统优化产生的问题,之所以维护副本和乱序都是为了提升性能

  • 可见性原因:处理器每个核使用自己的设施(寄存器,高速缓存,写缓冲),好处是可以批量合并,一起操作内存,所以内存并非实时保持最新
  • 重排序原因:编译器和处理器出于优化目的对没有数据依赖的指令进行重排序,比如两个不同变量赋值先后没关系,可以增加并行度

执行效果(Within-Thread As-If-Serial),即单线程观测效果符合顺序预期,但是多线程可能观测到不同结果
比如A线程内x=1和y=2

  • 可见性造成伪重排:没重排,确实x先赋值,但是xy位于不同缓存行,恰好y先LRU刷新回主存,造成B线程观察到只有y改变,似乎重排了
  • 真重排序:处理器执行时重排y先赋值,造成B线程观察到y先改变
    重排在A线程看来不受影响,在A后续使用xy变量时值都正确,而B线程可能先观测到y赋值

可见性:内存同步


Java内存模型JMM规定线程和内存间的交互规范
内存模型规范下,线程有主内存和工作内存之分。线程只和工作内存交互,工作内存线程独享
内存模型是概念模型,为了屏蔽硬件实现差异

  • 主内存,根据共享特性,可以近似理解为堆,物理上理解为内存
  • 工作内存,根据独占特性,可以近似理解为栈,物理上理解为各种高速缓存

线程有独立的工作内存(PC计数器,栈),存储了主内存共享变量副本
工作内存并非额外内存,只是栈,副本位于栈帧的局部变量表,只针对基本类型,引用指向的对象都是主存中的堆,是共享的
线程根据副本变量进行运算,并不能及时发觉主内存共享变量的变化

内存模型八种原子操作

1
2
3
4
共享变量    主内存    工作内存    变量副本      执行引擎
lock/unlock
------------read-------->----load--->-----use----->
<-----write---<---------store-------<-----assign--

内存屏障(Memory Barriers)


happens-before先行发生规则,规则适用于同一线程/不同线程的两个操作,规定了偏序,即后一操作能感知到前一操作造成的改变

内置的天然先行规则

  • 同一线程内,后面执行代码能感知前面的代码效果
  • 先解锁才能再加锁
  • volatile前面写操作先于后续读操作,前面读操作先于后续读写操作
  • 线程start后才能执行逻辑,逻辑在终止之前
  • 线程中断先于后续中断检测
  • 对象初始化先于finalize
  • 先行传递性

happens-before可看作是上层概念规则,语义上容易理解,实现上使用处理器的指令防止重排序造成的后果即内存屏障
内存屏障作用

  • 有序性:禁止跨越屏障重排序
  • 可见性:强制刷新内存,现有缓存失效

内存屏障类型

  • LoadLoad:Load1;LoadLoad;Load2屏障两个Load之间,确保前面先Load
  • StoreStore: Store1;StoreStore;Store2
  • LoadStore: Load1;LoadStore;Store2
  • StoreLoad: Store1;StoreLoad;Load2

volatile重排序保障机制:内存屏障

  • 写操作前插入StoreStore, 写操作后插入StoreLoad
  • 读操作前插入LoadLoad, 读操作后插入LoadStore
    宗旨是自己操作前,先让前面操作完; 自己没操作完,阻止后面操作

volatile可见性实现细节:Lock前缀指令+MESI协议
写操作完成后立刻发送Lock前缀指令,锁定缓存把结果写回主内存,同时MESI缓存一致性协议嗅探失效,下次读就从主内存加载

final和synchronized同样基于内存屏障

  • 可见性:进入同步块时有Load屏障,保证读取最新数据,退出同步块时有Store屏障,保证写回最新数据
  • 有序性:进入同步块时有Acquire屏障,退出同步块时有Release屏障,保证块内不会和块外重排序