一、CPU缓存给多线程带来的挑战
我们先看一段简单的程序:
public class VolatileTest {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
Thread.sleep(1000);
t.stop = true; // 语句1
}
}
class MyThread extends Thread {
boolean stop = false;
@Override
public void run() {
long startMs = System.currentTimeMillis();
while(!stop) {
}
long endMs = System.currentTimeMillis();
System.out.println(endMs - startMs);
}
}
上面这段代码,按理说,我们在语句一将t.stop
设置为true
时,MyThread
应该停止运行,但是多次执行程序后,我们发现并不是这样的,当我们将t.stop
设置为true
时,MyThread
仍在运行!
二、CPU缓存与可见性
上文的程序,导致结果与预期不符的是CPU缓存导致的。
MyThread
对象存放在内存(堆)中,程序在执行时为了提高执行效率,会将部分程序内容从内存加载到CPU缓存中。线程(CPU)在修改变量的值以后,也不会马上将值冲刷回主内存,而其它线程也并不会每次都去主内存读取值,有可能读取自己CPU缓存那一份旧的数据。因此,一个线程更新了某个值,另一个线程可能“看不到”,这就导致了可见性问题。
三、volatile解决可见性问题
Java的volatile关键字,可以解决以上可见性问题。
Java语言规范保证:每次读取一个volatile变量时,都会从主内存读取,每次更改一个volatile变量时,都会立马写会主内存。
通过volatile关键字,我们能保证,一个线程更新了一个变量,另一个线程能够立马看到。
我们稍微修改一下上面的程序:
public class VolatileTest {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
Thread.sleep(1000);
t.stop = true;
}
}
class MyThread extends Thread {
volatile boolean stop = false;
@Override
public void run() {
long startMs = System.currentTimeMillis();
while(!stop) {
}
long endMs = System.currentTimeMillis();
System.out.println(endMs - startMs);
}
}
这样子,程序在执行t.stop = true
后,MyThread
会立即停止。
四、volatile与有序性
什么是有序性?CPU在执行指令时,并不会按顺序执行,如果它认为,调换两个命令的顺序并不会影响程序的结果,那么为了提高效率,它可能会将这两条命令的执行顺序调换(重排序)。
class Date {
private int year;
private int month;
private int day;
public void setDate(Date date) {
this.year = date.year; // 语句1
this.month = date.month; // 语句2
this.day = date.day; // 语句3
}
public void getDate(Date date) {
date.day = this.day; // 语句4
date.month = this.month; // 语句5
date.year = this.year; // 语句6
}
}
以上程序,调用setDate()
方法是,语句1,2,3哪一条先被调用无法得到保证。调用getDate()
方法时,语句4,5,6哪个条先被调用无法得到保证。
volatile在保证可见性的同时,也保证了有序性。
Java语言规范保证:
- 一个线程在读取一个volatile变量时,所有的对这个线程可见的变量都会重新从内存中加载;
- 一个线程在写一个volatile变量时,这个线程在执行完以后,会将cpu缓存写回主内存;
- 对于volatile读操作,不允许将后面的读操作重排序到volatile读操作之前;
- 对于volattile写操作,不允许将前面的写操作重排序到volatile写操作之前。
因此,我们如果要保证上面程序的有序性,那么可以将day
变量标记为volatile
:
class Date {
private int year;
private int month;
private volatile int day;
public void setDate(Date date) {
this.year = date.year; // 语句1
this.month = date.month; // 语句2
this.day = date.day; // 语句3
}
public void getDate(Date date) {
date.day = this.day; // 语句4
date.month = this.month; // 语句5
date.year = this.year; // 语句6
}
}
对于写操作,我们就可以保证:语句1,2一定会在语句3之前执行,且year
和month
在语句3执行完后会和day
一样被写回主内存;
对于读操作,我们可以保证:语句4,5一定在语句6之前执行,且year
和month
在语句4执行完以后会更新为主内存的最新值。
五、volatile不保证原子性
volatile
并不保证原子性。例如:
volatile counter = 1;
counter = counter + 1; //语句1
语句1在执行时,如果读取完counter后,有其它线程修改了counter的值,那么程序就会出现预期之外的结果。