本文参考 黑马Java并发编程
Volatile
一个案例
在看volatile之前,我们先来看两段有点 “诡异” 的代码👇
//①号
class UnstoppableTest {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}
}
//②号
//I_Result r 是一个对象,有一个r1属性保存结果
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
//r.r1的结果可能是 0 !
要知道这两段代码为什么会出现这样的结果,以及如何解决,我们需要从Java的内存模型讲起👇
Java内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
来看下结构图:
JMM围绕建立的并发三大特性:
- 原子性:保证操作要么全部执行要么全部不执行
- 可见性:多个线程访问同个变量时,一个线程修改了变量值,其它线程能立刻看到修改的值
- 有序性:保证指令不会受 cpu 指令并行优化的影响;JVM在不影响正确性的前提下,可以调整语句的执行顺序,称为指令重排
这里原子性我们先不讲,volatile不能保证原子性
分析问题,解决问题
简单了解了JMM长啥样后,我们来看下前面那些代码的问题出在哪了?
首先来看看①号问题:
根据结构图,我们可以看到每个线程都一份共享变量的缓冲副本,线程t在第一次读取变量run
时是在主内存中拷贝一份到自己的缓冲区中,由于之后都没有再修改过这个run
变量,所以其它线程对run
的改变对线程 t 是不可见的!
那么该如何保证变量run
的可见性呢?方法有以下几种:
① 使用Synchronized加锁
② 打印run
变量(本质上与①一样)
③ 在run
变量上添加 **volatile **关键字
class UnstoppableTest {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t = new Thread(() -> {
while (true) {
synchronized (obj) {
if (!run) break;
}
// System.out.println(run);
}
});
t.start();
Thread.sleep(1000);
synchronized (obj) {
run = false;
}
}
}
解决完①号问题后,我们来看②号问题:
②号问题主要是多线程环境下指令重排引起的,我们来模拟一下0出现的场景:
方法actor2的语句被调换顺序 👇
// 线程2 执行此方法
public void actor2(I_Result r) {
ready = true;
num = 2;
}
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
- 线程2执行到
ready = true
时间片到了,切换到线程1运行 - 线程1执行判断进入if语句:
r.r1 = 0 + 0;
结束运行
分析完后来解决,解决的方法是前一个例子一样,Synchronized和volatile都能保证有序性
原理
那,它们是如何保证可见性
和有序性
的呢?
首先是Synchronized
- 保证可见性的方法是JVM规定线程加锁时,需要清空线程工作内存区,从主内存读取最新变量值,解锁时,把工作内存区的所有变量值写回主内存,这期间该线程获取着锁,所以不用担心线程安全问题;
- 保证有序性的方法在于加锁后临界区内代码执行是单线程的,即使重排,其它线程在阻塞中,无法进行操作,所以不会出现问题;这不是严格意义上的有序,语句仍可能会被重排;
虽然synchronized能解决可见性,但每次使用都需要申请Monitor对象并关联,有没有一种更轻量级的办法呢?欸,volatile在这里是一种更不错的选择;
那 volatile 又是如何保证可见性
和有序性
的呢?
答案是内存屏障:
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
- 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
volatile的应用——DCL单例模式
我们先来看看下面的DCL单例模式实现代码有没有问题
public final class Singleton{
private Singleton(){}
private static Singleton INSTANCE = null;
public static Singleton getInstance(){
if(INSTANCE == null){
synchronized(Singleton.class){
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
看着好像没什么问题,但需要注意的是变量INSTANCE
没有加volatile修饰,需要注意其可见性和有序性
虽然synchronized保护了内层的 IF 语句,但就像我们之前说的 synchronized内语句仍可能会被重排,只是单线程执行不影响结果
,别忘了外层循环可不是单线程执行!
通过字节码分析我们可以看到,会发生重排的关键代码在这一行
INSTANCE = new Singleton();
/* 字节码
new #3
dup
invokespecial #4 --执行构造方法
putstatic #2 --赋值给INSTANCE
*/
这里的第三行和第四行可能会被重排,就可能会发生一下情况👇
线程1 线程2
thread-1: 进入IF①
thread-1: 进入synchronized
thread-1: 进入IF②
thread-1: 发生指令重排
thread-1: INSTANCE=未执行构造方法的Singleton对象
thread-2: 进入IF①
thread-2: 返回INSTANCE (错了)
thread-1: 该Singleton对象执行构造器方法
可以看到线程2获取到的Singleton对象是没有执行构造方法的初始对象
所以,DCL中,我们必须使用 volatile
修饰 INSTANCE
变量!
CAS与volatile
既然看完了volatile,就顺便来看看CAS,CAS是CompareAndSwap的缩写,它是一种乐观锁;
我们来了解下用到CAS的一个典型方法:AtomicInteger.compareAndSet()
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
可以看到这是底层实现的,它完成了CPU层面的一次比较并交换的原子操作(锁住总线)
CAS的思想是,读取当前值,进行一次比较并交换,这是一次原子操作,如果比较后发现值没有变,就交换/set值,如果发生变化就不变,在循环中一直尝试;
代码展示:
class mytest1 implements Account {
private AtomicInteger balance;
public mytest1(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
}
CAS离不开volatile,因为它需要保证每次读到的值都是内存中的最新值👇
Q.E.D.