Java中的volatile关键字

Posted diuxie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的volatile关键字相关的知识,希望对你有一定的参考价值。

public class TaskRunner {

private static int number;
private static boolean ready;
private static class Reader extends Thread {

    public void run() {
        while (!ready) {
            Thread.yield();
        }
        System.out.println(number);
    }
}
public static void main(String[] args) {
    new Reader().start();
    number = 42;
    ready = true;
}

}
TaskRunner类维护两个简单的变量。在它的main方法中,它创建了另一个线程,只要它是false,它就会在ready变量上自旋。当变量变为true时,线程将打印number变量。

我们期望这个程序在短暂的延迟后简单地打印42。然而,实际上这个延迟可能会更长。它甚至可能永远挂起,甚至打印0。

这些异常的原因是缺乏适当的内存可见性和重排序,贴合本文来说,就是没有使用volatile关键字修饰变量。

内存可见性
简单来说,多线程运行在多个CPU上,而每个线程都会有自己的的cache,因此无法保证从主存中读取数据的顺序,即无法保证各个CPU上的线程读取的变量数据一致。

结合上面的程序,主线程在其核心缓存中保留了ready和number的副本,而Reader线程也是同样保留了副本,之后主线程更新缓存值。在大多数现代处理器上,写入请求在发出后不会立即应用。事实上,处理器倾向于将这些写入排在一个特殊的写入缓冲区中。一段时间后,它们会一次性将这些写入应用到主内存中。

因此当主线程更新number和ready变量时,无法保www.pizei.comReader线程会看到什么。换句话说,Reader线程可能会立即看到更新的值,或者有一些延迟,或者根本不会。

重排序
上面提到过,除了一直死循环外,程序还有小概率打印出0,这就是重排序的原因。在CPU执行指令时,先更新了ready变量然后执行的线程操作。

重新排序是一种用于提高性能的优化技术,不同的组件可能会应用这种优化:

处理器可以按程序顺序以外的任何顺序刷新其写缓冲区
处理器可能会应用乱序执行技术
JIT编译器可以通过重新排序进行优化
volatile关键字
那么volatile关键字干了什么呢?

volatile关键字在汇编阶段对变量加上 Lock前缀指令,通过 MESI缓存一致性协议来保证线程之间的可见性,任意线程对变量的修改都会被同一时间同步到所有读取该变量的线程CPU上,简单来说,一个改了就能保证所有的都改了。

这里先看汇编层的Lock指令,早期CPU采取锁总线的方式来实现这个指令,仲裁器选择一个CPU独占总线,从而使其他CPU无法通过总线与内存通讯,实现原子性;当然这种方式效率低,现在一般采用cache locking,这种场景下的数据一致是通过MESI缓存一致性协议来完成的。

这里不再详细说明缓存一致性协议,主要思想是CPU会不断嗅探总线上的数据交换,当一个缓存代表它所在的CPU去读写内存时,其他CPU都会得到通知,从而同步自己的缓存。

在Java内存模型中,存在着原子操作,这些原子操作与Java内存模型控制并发有着关键作用。

read(读取):从主内存读取页游数据
load(载入):将主内存读取到的数据写入工作内存,即缓存
use(使用):从工作内存读取数据来计算
assign(赋值):将计算好的值重新赋值到工作内存中
store(存储):将工作内存数据写入主内存
write(写入):将store过去的变量值赋值给主内存中的变量
lock(锁定):将主内存变量加锁,标识为线程独占状态
unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
在volatile关键字修饰下,store和write操作必须是连续的,组合成了原子操作,修改后必须立即同步到主内存,使用时必须从主内存刷新,由此保证volatile可见性。

同时,volatile关键字也采用内存屏障来禁止指令重排。volatile变量的内存可见性影响超出了volatile变量本身。

更具体地说,假设线程A写入一个volatile变量,然后线程B读取同一个volatile变量。在这种情况下,在写入volatile变量之前对A可见的值将在读取volatile变量后对B可见:

happens-before.png

从技术上讲,对volatile字段的任何写入都发生在同一字段的每次后续读取之前。这是Java 内存模型的volatile变量规则。

由于内存排序的长处,有时我们可以捎带volatile的可见性属性另一个变量。例如,在我们的示例中,我们只需要将ready变量标记为volatile:

public class TaskRunner {

private static int number; // not volatile
private volatile static boolean ready;
// same as before

}
在读取ready变量之后,任何在将ready变量写为true之前的内容对任何内容都是可见的。因此,number变量会捎带上ready变量强制执行的内存可见性。简而言之,即使它不是volatile变量,它也表现出volatile行为。

通过利用这些语义,我们可以将类中的少数变量定义为volatile并优化可见性。

以上是关于Java中的volatile关键字的主要内容,如果未能解决你的问题,请参考以下文章

Java中最简单易懂的volatile关键字示例

面试:说说Java中的 volatile 关键词?

Java中的volatile关键字

Java中的volatile关键字

Java中的volatile关键字为什么不是不具有原子性

Java中的volatile关键字