深入理解并发编程基础类Volatile特性

简介

大家先仔细看下这张图。


volatile、CAS作为基础类被很多组件(锁、线程池、阻塞队列、并发容器)所依赖和使用。

可见它在整个并发编程领域的地位!

今天我们一起来聊一聊这个基础类的相关特性。

volatile的作用

能保证可见性,在“一写多读”场景下比synchronized性能更好,因为不需要线程切换与调度(JMM的线程和OS的线程为一对一匹配:线程模型)。

多写场景下,无法保障”原子性“ 的原因分析:

先看这段代码:

public static volatile int race = 0;
public static void increase() {
  race++;
}


编译后的字节码:


public static void increase();
Code:0: 
getstatic #2 // Field race:I3: 
iconst_14:
iadd5:
putstatic #2 // Field race:I8:
return


volatile方式的i++,总共是四个步骤:

i++实际为load、Increment、store、Memory Barriers 四个操作。

我们从字节码层面来分析可能会导致保障不了原子性原因:

当getstatic指令把race的值取到操作栈顶的时候,volatile关键字保障了race在此时是正确的,但是在执行iconst_1、iadd的时候,其他线程很有可能已经把race值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存。

所以这种情况下还是需要加锁(synchronized或JUC中的原子类)来保障原子性!

应用场景:

CopyOnWriteArrayList

作为一写多读的应用,并发包的copyonArrayList有典型的应用。

它在修改数据的时候,会对整个集合的数据复制出来,对写加锁,修改完成后,再用setArray把array指向新的集合。

使用volatile可以尽快让其他线程感知到array的变化,不进行指令重排。


可见性实现原理



先看一段volatile实例变量的代码:

public class MySingleton {
  private static volatile MySingleton instance = null;
  public static MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
  return instance;
 }
 public static void main(String[] args) {
  MySingleton.getInstance();
 }
}

汇编代码


0x00000000027df0d5: lock add dword ptr [rsp],0h ;*putstatic instance; - com.dunzung.demo.MySingleton::getInstance@13 (line 9)


计算机处理过程如下:

如果一旦对声明了volatile的变量进行了写操作,那么JVM就会像处理器发送一条Lock前缀的指令,将这个变量所在的缓存行数据回写到主内存。

其他处理器通过“嗅探”机制,发现自己缓存行变量内容(处理器)对应的主存地址发生了变化,就会将当前处理器的缓存行设置为无效状态。

那么就会重新从主存中读取变量值,这其实就保证了可见性!

写读的内存语义

写的内存语义

当写一个volatile变量时,JMM会将此变量刷新到主内存。

读的内存语义

当读一个volatile变量时,JMM会把线程对应的本地内存置为无效状态,下次强制从主存读取变量值。

为了实现这两者内存语义,JMM制定了“重排序规则表”

  • 第一个变量是普通的读或写,只要第二个操作是volatile写,那么不能重排序
  • 第一个操作是volatile读,其他操作都不能重排序
  • 第一个操作是volatile写,第二个操作是volatile读或写都不能重排序

为了实现这两种内存语义,JMM限制了编译器和处理器的重排序。

底层是通过“内存屏障”来禁止特定类型的指令重排序。


如何防止指令重排

JMM通过happens-before机制向程序员提供有序性保证,防止指令重排。

两个操作的有序性可以是一个线程内发生或两个不同线程发生。只要符合Happens-before规则。

Happen-before规则如下:

  1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
  2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。
  4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
  6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
  8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。


本文完!

写到最后

如果这篇文章你看了对你有帮助或启发,麻烦关注、点赞一下作者。你的肯定是作者创作源源不断的动力。谢谢!

同时推荐大家关注作者公众号:陶朱公Boy

里面不仅汇集了硬核的干货技术、还汇集了像左耳朵耗子、张朝阳总结的高效学习方法论、职场升迁窍门、软技能。希望能辅助你达到你想梦想之地!

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章