美文网首页
Java 锁机制详解(二)volatile

Java 锁机制详解(二)volatile

作者: Parallel_Lines | 来源:发表于2020-04-22 19:31 被阅读0次

上接 Java 锁机制详解(一)synchronized

一、 多线程隐患

1. 内存可见性

在赋值变量时,会经历数据由 CPU 写入到 内存 的过程,由于现代 CPU 一般都会有多级缓存,导致写指令可能并不能立即将数据写入到内存。如下图:

CPU写入数据到内存.png

举个例子:

public class Demo {
    private int a = 0;

    public void write() {
        a = 1;
    }

    public void read() {
        Log.e("TAG", a);
    }
}

线程 A 调用 write(),线程 B 调用 read(),返回的结果可能是 1 也有可能是 0,参考上图可知,线程 B 调用 read() 时,数据可能写入到了多级缓存里,而没有写入到内存中,导致读取到的值是 0。

多线程的程序中,一个线程写入的数据不能及时反映到另一个线程中,此时会说一个线程对变量的修改对另一个线程不可见,即 内存可见性

放到 Java 里,对内存可见性又做了一层抽象:

JVM 规定所有变量都存在 主存 中,但是每个线程又有自己的 工作内存,线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在 同步回主内存

即线程执行时,对于读操作,会先从主存中读值,然后赋值到工作内存的副本中,最后传给 CPU。对于写操作,CPU 会先写入到工作内存的副本,然后再传回给主存,此时主存才真正更新。即 Java 的内存可见性

方便理解,主存即可当作内存可见性中的内存,工作内存即可视为内存可见性中的 CPU 多级缓存。

2. 指令重排

看个例子

public class Demo {
    int a = 0;
    boolean flag = false;

    /**
    * A线程执行
    */
    public void writer(){
        a = 1;                  // 1
        flag = true;            // 2
    }

    /**
    * B线程执行
    */
    public int read(){
        if(flag){               // 3
          return a;             // 4
        }
    }
}

线程 A 调用 writer(),线程 B 调用 read(),因为指令重排的影响,read() 返回值可能是 0,也可能是 1。

为什么会这样呢?下面分析原因。

CPU 和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化。举个例子:

int a = 1 ;    //1  
int b = 2 ;    //2  
int c = a + b; //3

由于语句 1 和语句 2 不存在依赖关系,因此在重排序时,语句 1、2 可以随意排序,只要总位于语句 3 前即可。

这在单线程是没有问题的,但是多线程情况下,就会有问题。比如指令重排一开始的例子,可能执行顺序为:

2 → 3 → 4 → 1 结果为 0.

可见指令重排在多线程情况下可能导致原语义的破坏。

二、 volatile 解析

正是因为多线程环境下存在 内存可见性指令重排 等问题,所以诞生了 volatile 关键字。

首先对于 内存可见性 导致的问题,volatile 修饰的成员变量在每次被线程访问时,都强迫从主存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到主存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值,这样也就保证了同步数据的 可见性

然后对于 指令重排 导致的问题,volatile 做了如下规定:

  1. 在 volatile 变量的写入指令之前,对其它变量的读写指令不能重排到该指令之后。
  2. 在 volatile 变量的读取指令之后,对其它变量的读写指令不能重排到该指令之前。

还是上边的例子:

public class Demo {
    int a = 0;
    volatile boolean flag = false;

    /**
    * A线程执行
    */
    public void writer(){
        a = 1;                  // 1
        flag = true;            // 2
    }

    /**
    * B线程执行
    */
    public int read(){
        if(flag){               // 3
          return a;             // 4
        }
    }
}

因为变量 flag 写入指令之前,其它变量的读写指令不能重排到该指令之后,所以语句 1 一定先语句 2 执行,那么 read() 结果一定为 1。

volatile 如何限制指令重排,涉及到内存屏障的知识点,不是本文重点。

三、 synchronized 与 volatile

既然说到 volatile,就必然提到几个与 synchronized 相关的问题。

1. synchronized 能防止指令重排序吗?

能,synchronized 保证原子性、有序性和可见性,只是代价高。

JVM 规定了 happens-before 规则,volatile 和 synchronized 可以防止指令重排序本质都是遵守此规则。关于 happens-before 后续会单出一篇博客来说明,这里可以不深究。

2. double check 单例是否需要使用 volatile?

public class SingletonClass { 

  private static SingletonClass instance = null; 

  public static SingletonClass getInstance() { 
    if (instance == null) { 
      synchronized (SingletonClass.class) { 
        if (instance == null) { 
          instance = new SingletonClass(); 
        } 
      } 
    } 
    return instance; 
  } 

  private SingletonClass() { } 
}

因为第一个 if 没有包入到 synchronized 中,所以 synchronized 的有序性对第一个 if 是不生效的。

对于 new SingletonClass(); 这个语句,并不是原子操作,可以分为以下三个步骤:

  1. 为 instance 分配内存;
  2. 调用构造函数初始化成员变量;
  3. 将 instance 指向分配的内存空间。

其中步骤 2、3 的顺序因为指令重排的原因,是不确定的。

这就可能发生如下情况:

  1. 线程 A 先将 instance 指向分配的内存空间,此时构造函数还未调用,就执行下一步。
  2. 线程 B 执行第一个 if 语句时,instance 不为空,于是返回、调用。但是实际上 instance 还未初始化,于是出错了。

将 instance 修饰为 volatile,实际上是保证了第一个 if 读的时候的有序性,防止了 instance 指令重排带来的隐患。

3. volatile 能保证原子性吗?

不能。举个例子:

// 定义
private volatile int a = 0;

private class IncreaseThread extends Thread {

    @Override
    public void run() {
        super.run();
        increase();
    }

    private void increase() {
        a++;
    }
}
    
// 执行
for (int i = 0; i < 100; i++) {
    new IncreaseThread().start();
}
Log.e("TAG", "a is " + a);

a 的值总小于 100,原因是因为自增不具备原子性,它由三个字操作构成:

  1. 读取原始值;
  2. 进行加1操作;
  3. 写入工作内存。

有可能出现线程 A 读取原始值之后,阻塞,线程 B 再读取原始值,然后线程 B 加 1 后写入,线程 A 加 1 后写入,俩次自增,实际结果只加了 1。

参考链接
https://www.zhihu.com/question/37601861
https://lotabout.me/2019/Java-volatile-keyword/
https://juejin.im/post/5a2b53b7f265da432a7b821c
https://www.jianshu.com/p/b4d4506d3585

[TOC]

相关文章

网友评论

      本文标题:Java 锁机制详解(二)volatile

      本文链接:https://www.haomeiwen.com/subject/xlnwihtx.html