volatile

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的’可见性’.可见性的意思是当一个线程修改一个共享变量时,另一个线程能读取到这个修改的值.

volatile变量修饰符使用恰当 的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度.

把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步. 锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对 一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入

volatile变量具有以下特性

  • 可见性: 对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

volatile的定义与实现原理

Java编程语言允许线程访问共享变量,为了 确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言 提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存 模型确保所有线程看到这个变量的值是一致的 – Java官方定义

有volatile变量修饰的共享变量进行写操作时,处理器会额外进行如下操作

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

volatile写的内存语义: 写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存. 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息

volatile读的内存语义: 读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量. 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile
变量之前对共享变量所做修改的)消息

为了实现volatile内存语义,JMM会限制编译器重排序和处理器重排序. 编译器在生成字节码时,会在指令序列中插入内存屏障来 禁止特定类型的处理器重排序.

JMM内存屏障插入策略:

  • 写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障
  • 读操作前插入LoadLoad屏障,读操作后插入LoadStore屏障

因为volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时, 选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM 在实现上的一个特点:首先确保正确性,然后再去追求执行效率

不同处理器针对内存屏障会有不同的优化,X86处理器下,除StoreLoad屏障外,其他的屏障都会被省略

由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以 确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和执行 性能上,volatile更有优势

文章链接 https://fangzongzhou.github.io/2020/10/13/计算机/技术栈/Java/并发编程/volatile/