Java多线程并发编程之volatile关键字解析
写在开篇之前
记得我有次面试的时候,问到了多线程这块,面试官问了我对于volatile关键字的理解,当时我是说在多线程并发编程中,对于共享变量,用volatile关键字可以保证每个线程读取变量值的时候都是最新的。但当他继续问我为什么volatile这个关键字可以保证读取的值是最新的时候,我发现原来我理解的可能还是不够深入。于是回来之后,我便查阅资料,深入研究了一下volatile关键字的作用。下面就分享一下我对volatile关键字的理解。
CPU高速缓存
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
并发编程中通常会遇到的三个问题
原子性问题,可见性问题,有序性问题。我们先具体看一下这三个概念:
1.原子性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
2.可见性:
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3.有序性:
即程序执行的顺序按照代码的先后顺序执行。一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
对于原子性,volatile关键字只能保证每次读取的是最新的值,但是没办法保证对变量的操作的原子性。Java只保证了对基本数据类型的读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
对于有序性,在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外还可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
我们回到主题,前面说volatile关键字可以保证可见性和有序性,下面介绍具体是如何保证的。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1、使用volatile关键字会强制将修改的值立即写入主存,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效),由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
2、volatile关键字能禁止进行指令重排序,所以volatile能在一定程度上保证有序性。
Volatile的使用场景:
需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。比如:状态标记量,双重校验
在Java中双重检查模式无效的原因是在不同步的情况下引用类型不是线程安全的。对于除了long和double的基本类型,双重检查模式是适用 的,如果将引用类型声明为volatile,双重检查模式就可以工作了。