深入理解volatile关键字
CPU和JMM
初识volatile关键字
- 以一个
demo
来直观的感受下volatile
关键字的作用。先将下面代码的中的volatile
去掉运行,然后再加上运行main
方法,观察两次运行的区别。
public class VolatileFoo {
// init_value的最大值
private final static int MAX = 5;
// init_value的初始值、
private static volatile int initValue = 0;
public static void main(String[] args) {
// 启动一个Reader线程,当发现local_value和init_value不同时,则输出init_value被修改的信息
new Thread(() -> {
int localValue = initValue;
while (localValue < MAX) {
if (initValue != localValue) {
System.out.printf("The initValue is updated to [%d]\n", initValue);
// 对localValue进行重新赋值
localValue = initValue;
}
}
}, "Reader").start();
// 启动一个Updater线程,主要用于对initValue的修改,对localValue>=5的时候则退出生命周期
new Thread(() -> {
int localValue = initValue;
while (localValue < MAX) {
// 修改initValue
System.out.printf("The initValue will be changed to [%d]\n", ++localValue);
initValue = localValue;
try {
// 暂时休眠,目的是为了使Reader线程能够来得及输出变化内容
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Updater").start();
}
}
- 可以观察到,没有
volitale
关键字,运行的情况如下。似乎Reader
线程从来没有执行过一般。其实,仔细观察你会发现程序并没有终止,其实是进入了死循环状态了。
The initValue will be changed to [1]
The initValue will be changed to [2]
The initValue will be changed to [3]
The initValue will be changed to [4]
The initValue will be changed to [5]
- 加上
volatile
关键字后,运行结果如下,这才是我们预期想要的结果。
The initValue will be changed to [1]
The initValue is updated to [1]
The initValue will be changed to [2]
The initValue is updated to [2]
The initValue will be changed to [3]
The initValue is updated to [3]
The initValue will be changed to [4]
The initValue is updated to [4]
The initValue will be changed to [5]
The initValue is updated to [5]
在说明为何会出现这种情况之前,首先得介绍下CPU结构和Java的内存模型。
CPU
CPU Cache
模型
- 首先,我们要清楚一件事。那就是**
CPU
所能访问的所有数据只能是计算机的主存(RAM)
,并且CPU
处理数据的速度远远大于内存的访问速度**。对于共享数据来说,这就会产生数据一致性问题。所有就有了CPU Cache
模型。 - 现在的
CPU Cache
的数量可以达到3
级了,点击计算机的任务管理器 -> 性能 -> CPU
是可以观察到这个三个缓存的,分别为L1,L2,L3
缓存。这其中L1
离CPU
最近,从中读取数据的速度也是最快的。 CPU
通过直接访问Cache
的方式替代直接访问内存的方式极大地提高了CPU
的吞吐能力。但是,这样将数据缓存起来,虽然提高了吞吐能力,但是带来的副作用就是数据一致性问题。
缓存一致性问题
-
解决缓存数据一致性问题,通常采取的方法无非就是如下两种:
- 通过总线加锁的方式;
- 通过缓存一致性协议。
-
第一种方法大致可以理解为
Java
中加同步锁的意思。这样就会阻塞其他CPU
对其他组件的访问,从而使得只有一个CPU
能够访问这个变量的内存。这大大地降低的CPU
的利用率,毕竟现在的计算机都是多核的了。 -
第二种方法简单的理解就是:各个缓存间共享变量的副本是一致的。如果哪一个缓存对变量进行了修改,修改完成后会立即将新的变量值刷新到主存中。并且会导致其它缓存中的变量副本失效,只能再次从主存中获取。显然,这种方式优于第一种方法。
JMM
Java
的内存模型(Java Memory Mode, JMM)
指定了Java
虚拟机如何与计算机的主存RAM
进行工作。它决定了一个线程对共享变量的写入何时对其他线程可见,定义了主内存和线程之间的抽象关系,如下:- 共享变量存储于主内存中,每个线程都可以访问;
- 每个线程都有私有的工作内存或者称之为本地内存;
- 工作内存只存储该线程对共享内存的副本;
- 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存中;
- 工作内存和
Java
内存模型都是抽象的概念,它其实并不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等。
深入volatile关键字
- 说
volatile
关键字,就不得不说并发编程;因为volatile
关键键主要作用就是数据可见性,解决数据一致性问题。 - 并发编程的三大特性:原子性、可见性和有序性。
原子性
- 简单的说就是一些操作,要么全部成功,要么全部失败。
- 就比如A给B转账500,A账户减500和B账户加500这两个操作必然是要成功都成功,要失败都失败。
可见性
- 当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的新值。
有序性
- 指程序代码在执行过程中的先后顺序。在
JVM
中不一定保证程序的执行顺序一定按照代码的抒写顺序,但是一定能保证程序执行的最终结果一致。 - 注意:在单线程情况下,这并不会有什么问题。但是在并发多线程的情况下,指令的重排序就可能会造成不可预期的后果。
JMM保证三大特性
JVM
采用内存模型的机制来屏蔽各个平台和操作系统之间内存访问的差异,以实现让Java
程序在各种平台下达到一致的内存访问效果。比如不管在什么操作系统中,int
类型的都是占用四个字节。Java
的内存模型规定了所有的变量都存在于主内存(RAM)
当中的,而每个线程都有自己的工作内存和本地内存,线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接操作主内存,并且每个线程都不能访问其他线程的工作内存和本地内存。- 首先明确一点的是:
volatile
不能保证原子性。但是可以保证可见性和一致性。 JMM
可以通过synchronized
关键字保证三大特性,也可以通过JUC
中的显示锁Lock
来保证。
深入解析volatile
volatile
关键字有两层语义:- 保证了不同线程之间对共享变量操作时的可见性;
- 禁止对指令进行重排序操作。但需要注意的是,只限于对
volatile
关键字修饰的变量。对于volatile
前后无依赖关系的指令则可以随便这么排序。
- 而通过对源码的解读,可以发现被
volatile
修饰的变量存在于一个lock;
的前缀。这个前缀实际上相当于是一个内存屏障,该屏障会为指令的执行提供如下几个保护:- 确保指令重排序时不会将其后面的代码排到内存屏障之前;
- 确保指令重排序时不会将其前面的代码排到内存屏障之后;
- 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成;
- 强制将线程工作内存中值的修改刷新至主内存中;
- 如果是写操作,会导致其它线程的工作内存中的缓存数据失效。
volatile的使用场景
- 开关控制利用。简单说就是
if
判断语句,当布尔变量修改时为了保证其它线程操作不会出现因数据不一致导致问题,用volatile
修饰该变量,保证判断时该变量都是主内存中最新的值。 - 状态标记利用顺序性的特点。利用禁止重排序的特点。
- 在单例模式中的
double-check
的应用。这里用兴趣的朋友可以自行了解,它的主要作用就是保证对象的初始化一定成功。防止有为初始化成功的对象返回,从而可能会导致NPE
,空指针异常。
volatile和synchronized的区别
- 使用上的区别
volatile
关键字只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量、常量等;synchronized
关键字不能用于对变量的修饰,只能用于修饰方法和代码块。volatile
修饰的变量可以为null
,但是synchronized
关键字的锁对象不能为null
。
- 对原子性的保证
volatile
不能保证原子性;synchronized
是一种排他的机制,因此被其修饰的同步代码块无法被中途打断,因此其能够保证原子性。
- 对可见性的的保证
synchronized
借助于JVM
的指令monitor enter
和monitor exit
对通过排他的方式使同步代码块串行化;在monitor exit
时所有共享资源都会刷新到主内存中。volatile
使用机器指令**lock;
**的方式迫使其他线程工作内存中的数据缓存失效,不得不到主内存中重新获取。
- 对有序性的保证
volatile
关键字禁止JVM
编译器以及处理器对其进行重排序,所以它能保证有序性。synchroinzed
是通过串行化的方式保证了有序性。但是在其修饰的方法或者代码块中依然可以进行指令重排序。
- 其它
volatile
不会使线程陷入阻塞;synchronized
会使线程进入阻塞状态。