【深入浅出Java多线程】volatile

122 阅读5分钟

在理解Java的volatile关键字之前,我们需要先了解CPU对数据的读写方式,指令的执行方式和Java内存模型的一些规定。

背景

CPU Cache

image.png

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。一般程序运行过程中的临时数据是存放在主存(物理内存)当中的,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

举个简单的例子,比如下面的这段代码:

i = i + 1

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

Java对内存模型的规定

image.png

Java内存模型的主要目的是定义程序中各种变量的访问规则。此处的变量(V ariables)与Java编程中所说的变量有所区别,不包括局部变量与方法参数,因为他们是线程私有的,不会被共享,不会存在竞争问题。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。

上诉内容中,主内存与工作内存只是一个抽象概念,主内存可以类比于物理硬件的主内存,工作内存可以类比于处理器的高速缓存。所谓主内存副本,并不是将对象完全复制一份,而是复制对象的引用、对象中某个在线程访问到的字段。

指令重排

指令重排是指处理器为了提高程序运行效率,可能会对输入代码进行优化,不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是保证程序最终执行结果和代码顺序执行的结果是一致的。

比如下面的代码

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。指令重排不会影响单个线程内程序执行的结果,但是多线程环境下可能会有问题。比如下面的代码

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

volatil的作用

可见性

所谓的可见性,是指当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//线程2
stop = true;

上述代码希望在线程2中对stop进行赋值进而控制 线程1中的程序的行为。但是这种写法可能并不能达到目标:

  1. 每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
  2. 那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

为保证对stop变量的修改能及时被其他线程感知,我们可以用 volatil 关键字修饰变量stop。

有序性

所谓的代码执行有序性,是指各个语句的执行先后顺序同代码中的顺序一致,即对部分代码禁止指令的重排。Java 内存模型是通过内存屏障(memory barrier)来禁止重排序的。

这些内存屏障会限制即时编译器的重排序操作。以 volatile 字段访问为例,所插入的内存屏障将不允许 volatile 字段写操作之前的内存访问被重排序至其之后;也将不允许 volatile 字段读操作之后的内存访问被重排序至其之前。