01.并发多线程-volatile

可见性

  • 多个线程并发读写一个共享变量的时候,有可能某个线程修改了变量的值,但是其他的线程看不到,也就是对其他线程不可见

工作原理

image

  • 主要作用是保证可见性以及有序性
  • 不能保证原子性

volatile 怎么保证可见性和有序性

内存可见性
  1. CPU的内存访问很慢,所以CPU有几层的高速缓存,加速内存访问速度
  2. Java的内存模型对上述又进行了一些列的抽象,JMM(java内存模型)规定所有的变量都在存在主内存的,每个线程又包含自己的工作内存
  3. 变量在工作内存中修改以后,就会强制将工作内存中的刷回主内存,主内存的变量值立马变成最新的值
  4. 其余线程中工作内存中的变量缓存直接强制失效过期,不允许直接读取和使用,当线程再次准备使用的时候,会在主内存中直接读取最新的值
有序性
  1. JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变
  2. 针对多线程出现的问题,加上volatile 会禁止重排序,可以确保程序的有序性
原子性
  1. volatile虽然不能保证原子性,但是在某一些条件下,还是能提供原子性的
  2. 如读 64 位数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和 long 就是原子的。

底层时间的机制

  • 如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀的指令
  • lock前缀指令实际相当于一个内存屏障
  • 内存屏障提供了一下的功能
    1. 重排序时,不能把后面的指令重排序到内存屏障之前的位置
    2. 使得本CPU的Cacahe写入内存
    3. 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。

应用举例

1. 状态量标记
int a = 0;
volatile bool flag = false;

public void write() {
    a = 2;              //1
    flag = true;        //2
}

public void multiply() {
    if (flag) {         //3
        int ret = a * a;//4
    }
}
复制代码
2.单例模式的实现,典型的双重检查锁定(DCL)
class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}
复制代码

这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给instance加上了volatile。

相关的面试题目
1、Java 中能创建 volatile 数组吗?
  • 能,不过创建的是一个指定数组的引用,而不是整个数据,如果改变引用指向的数据,将会受到volatile的保护,但是如果多个线程同时改变数据的元素,volatile标识符就不能祈祷之前的保护作用了
2、volatile 能使得一个非原子操作变成原子操作吗?
  • 一个典型的例子是在类中的有一个long类型的成员变量,如果你知道该成员变量会被多个线程访问,那么最好将这个成员变量设置为volatile,因为java中读取long类型不是院子的,需要分成两步,如果一个线程正在修改该long变量的值,另一个此案城可能只能看到该值的一般(前32为)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。

猜你喜欢

转载自juejin.im/post/5cfe4975f265da1b8811d1ba
今日推荐