关于volatile:理解Volatile关键字其实看这一篇就够了写的非常细致

37次阅读

共计 3118 个字符,预计需要花费 8 分钟才能阅读完成。

前言

volatile 是 Java 虚拟机提供的轻量级的同步机制。

volatile 关键字作用是什么?

两个作用:

1. 保障被 volatile 润饰的共享变量对所有线程总数可见的,也就是当一个线程批改了一个被 volatile 润饰共享变量的值,新值总是能够被其余线程立刻得悉。

2. 禁止指令重排序优化。

volatile 的可见性

对于 volatile 的可见性作用,咱们必须意识到被 volatile 润饰的变量对所有线程总数立刻可见的,对 volatile 变量的所有写操作总是能立即反馈到其余线程中;

上面来测试一下,此时的还未 initFlag 被 volatile 润饰。

private boolean initFlag = false;
 
public void test() throws InterruptedException{Thread threadA = new Thread(() -> {while (!initFlag) { }
        String threadName = Thread.currentThread().getName();
        System.out.println("线程" + threadName+"获取到了 initFlag 扭转后的值");
    }, "threadA");
 
    // 线程 B 更新全局变量 initFlag 的值
    Thread threadB = new Thread(() -> {initFlag = true;}, "threadB");
    
    // 确保线程 A 先执行
    threadA.start();
    Thread.sleep(2000);
    threadB.start();}

执行后果:控制台只打印了 “ 线程 threadB 扭转了 initFlag 的值 ”,且程序并未终止。

此时 initFlag 曾经被 volatile 关键字润饰了

private volatile boolean initFlag = false;
 
public void test() throws InterruptedException{Thread threadA = new Thread(() -> {while (!initFlag) { }
        String threadName = Thread.currentThread().getName();
        System.out.println("线程" + threadName+"获取到了 initFlag 扭转后的值");
    }, "threadA");
 
    Thread threadB = new Thread(() -> {
        initFlag = true;
        String threadName = Thread.currentThread().getName();
        System.out.println("线程" + threadName+"扭转了 initFlag 的值");
    }, "threadB");
 
    // 确保线程 A 先执行
    threadA.start();
    Thread.sleep(2000);
    threadB.start();}

执行后果:

线程 threadB 扭转了 initFlag 的值
线程 threadA 获取到了 initFlag 扭转后的值

并且程序曾经完结了。

这个案例充分说明了 volatile 的可见性作用。

volatile 无奈保障原子性

来个案例阐明所有:

private static volatile int count = 0;
/**
 * count 尽管被 volatile 关键字润饰,然而后果并不是 50000,而是小于等于 50000
 **/
public static void main(String[] args) throws InterruptedException{
 
    // 开启 10 个线程,别离对 count 进行自增操作
    for (int i = 0; i < 10; i++) {Thread thread = new Thread(() -> {for (int j = 0; j < 5000; j++) {count++;    // 先读,再加,不是一个原子操作}
        });
        thread.start();}
    Thread.sleep(2000);
    
    System.out.println("count==" + count);
}

count 尽管被 volatile 关键字润饰了,然而输入的后果会小于等于 50000,足以阐明了 volatile 无奈保障原子性。

volatile 禁止重排优化

volatile 关键字另一个作用就是禁止指令重排优化,从而防止多线程环境下程序呈现乱序执行的景象。

内存屏障,又称内存栅栏,是一个 CPU 指令,它的作用有两个,一是保障特定操作的执行程序,二是保障某些变量的内存可见性(利用该个性实现 volatile 的内存可见性)。因为编译器和处理器都能执行指令重排优化。如果在指令间插入一条 Memory Barrier 则会通知

编译器和 CPU,不论什么指令都不能和这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier 的另外一个作用是强制刷出各种 CPU 的缓存数据,因而任何 CPU 上的线程都能读取到这些数据的最新版本。

总之,volatile 变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。

上面看一个十分典型的禁止重排优化的例子,如下:

// 禁止指令重排优化
private volatile static VolatileSingleton singleton;
 
public static VolatileSingleton getInstance(){if(singleton != null){synchronized (VolatileSingleton.class){if(singleton != null){
                // 多线程环境下可能会呈现问题的中央
                singleton = new VolatileSingleton();}
        }
    }
    return singleton;
}

新 new 一个对象是分为三步来实现:

memory = allocate();//1. 调配对象内存空间

instance(memory);//2. 初始化对象

singleton = memory;//3. 设置 singleton 对象指向刚调配的内存地址,此时 singleton != null

因为步骤 1 和步骤 2 间可能会重排序,如下:

memory = allocate();//1. 调配对象内存空间

singleton = memory;//3. 设置 singleton 对象指向刚调配的内存地址,此时 singleton != null

instance(memory);//2. 初始化对象

因为步骤 2 和步骤 3 不存在数据依赖关系,而且无论重排前还是重排后程序的执行后果, 在单线程中并没有扭转,因而这种重排优化是容许的。然而指令重排只会保障串行语义的执行的一致性(单线程),但并不会关怀多线程间的语义一致性。所以当一条线程拜访 singleton 不为 null 时,因为 singleton 实例未必已初始化实现,也就造成了线程平安问题,volatile 禁止 singleton 变量被执行指令重排优化.

volatile 重排序规定表

能够总结为三条:

  1. 当第二个操作是 volatile 写时,不论第一个操作是什么,都不能重排序。这个规定确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。
  2. 当第一个操作是 volatile 读时,不论第二个操作是什么,都不能重排序。这个规定确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。
  3. 当第一个操作是 volatile 写,第二个操作是 volatile 读时,不能重排序。

最初

感激你看到这里,看完有什么的不懂的能够在评论区问我,感觉文章对你有帮忙的话记得给我点个赞,每天都会分享 java 相干技术文章或行业资讯,欢送大家关注和转发文章!

正文完
 0