参考:http://tutorials.jenkov.com/java-concurrency/volatile.html
变量可视性问题
Java的volatile关键字保证了线程间变量改变时的可视性问题。这么说可能有一点抽象,下面详细描述:
在一个多线程应用中,当线程操作非volatile(不稳定的)变量时,出于性能考虑,每一个线程都会从主存中复制一份变量的副本到一个执行这个线程的CPU的缓存中。如果你的电脑包含多个CPU,每一个线程可能都运行在一个不同的CPU上。这意味着,不同的线程会复制这个变量的副本到不同的CPU缓存中。下图说明了这一点:
非volatile变量不保证JVM何时从主存中读取变量副本到CPU缓存中时,或者将CPU缓存写入到主存中。这会造成下列问题:
想象一下,一个对象如下代码声明含有一个counter变量,两个或者多个线程交叉访问它。
public class SharedObject {
public int counter = 0;
}
当只有线程A增加这个counter变量,但是线程A和线程B会时不时的读取这个counter变量。
如果这个counter变量没有被声明为volatile
,将会不保证当这个counter变量何时从CPU的缓存中回写至主存。这意味着,counter变量在CPU缓存中的值会与主存中不一致,下图描述了这种情景:
线程看不到变量最新的值的问题是因为这个变量还没有被另一个线程回写到主存中。
Java volatile可视性保证
Java的volatile
关键字旨在解决变量的可视性问题。通过将counter变量声明为volatile,所有对这个变量的写入操作都会立即被回写入主存中。同样的,所有对变量的读取都会从主存中直接读取。如下是counter变量声明为volatile的样子:
public class SharedObject {
public volatile int counter = 0;
}
变量声明为volatile保证了写入变量时其他线程关于这个变量的可视性问题。
在上述场景中,当线程A修改了counter变量,另一个线程B读取counter但是不修改它,声明这个counter变量为volatile足够保证当counter变量修改时,线程B关于这个counter变量的可视性。
但是当AB线程都对counter变量做修改时,只声明counter变量为volatile是不够的
充足的volatile可实行保证
事实上Java的volatile保证超过了volatile变量本身。如下所述:
- 如果线程A写入了volatile变量,线程B随后读取同一个的volatile变量,则线程A写入这个volatile变量之前的所有可视的变量,在线程B读取volatile变量值之后对线程B都是可视的。
- 如果线程A读取一个volatile变量,当线程A读取volatile变量时线程A可见的所有变量都将从主存中重新读取。
下面代码进行说明:
public class VolatileTest {
private static int count0 = 1000;
/**
* 排除cache line的影响
*/
private static long l1, l2, l3, l4, l5, l6, l7, l8 = 0L;
private static volatile int flag = 0;
/**
* 排除cache line的影响
*/
private static long l11, l12, l13, l14, l15, l16, l17, l18 = 0L;
private static int count1 = 1000;
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread thread1 = new Thread() {
@Override
public void run() {
// 短暂睡眠以使thread2进入循环
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count0 = 0;
flag = 1;
count1 = 0;
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
while (flag != 1) {
}
if (count0 != 0) {
String name = Thread.currentThread().getName();
System.out.println("ERROR count0: " + name + " " + count0);
}
if (count1 != 0) {
String name = Thread.currentThread().getName();
System.out.println("ERROR count1: " + name + " " + count1);
}
}
};
thread1.start();
thread2.start();
thread1.join();
thread2.join();
/**
* 保证变下面三个操作被所有线程感知到
*/
synchronized (VolatileTest.class) {
count0 = 1000;
count1 = 1000;
flag = 0;
}
}
}
}
flag变量是一个volatile变量,count0和count1变量是非volatile变量,线程1按顺序写入count0、flag、count1变量。对于线程1来说,写入flag=1时,count0是可视的,那么线程2在读取flag之后,count0对于线程2也是可视的(即线程2读取到的count0是最新的值 0),但count1因为在flag之后操作,所以count1对于线程2来说并不是可视的,会造成偶尔count1不等于0的情况。运行可以得到如下结果
ERROR count1: Thread-47 0
ERROR count1: Thread-63 0
ERROR count1: Thread-77 0
......
指令重排给volatile带来的问题
JVM和CPU允许在保持语义不变的情况下调整指令顺序以提高性能。如下例:
int a = 1;
int b = 2;
a++;
b++;
可能会在不改变语义的情况下被修改为如下的顺序
int a = 1;
a++;
int b = 2;
b++;
在上个章节中的代码
···
private static int count0 = 1000;
private static volatile int flag = 0;
private static int count1 = 1000;
···
count0 = 0;
flag = 1;
count1 = 0;
···
可能会被重排为如下顺序
flag = 1;
count0 = 0;
count1 = 0;
这会造成之前count0是在更新flag时是可视的,但现在不是。count0的新值可能不会对其他线程可视。这个顺序下语义发生了变化。
Java中是如何解决这个问题的呢?
Java volatile关键字的Happens-Before 保证
为了解决指令重排问题,处理提供可视性保证之外,Java的volatile关键字提供了一个"happens-before"保证。"happens-before"保证如下:
- 对于其他变量的读取或者写入如果发生在写入volatile变量之前,则不能被重排序到写入volatile变量之后。即发生在写入volatile变量之前的对其他变量读取写入操作,保证在写入volatile变量之前发生。注意,当读取或者写入其他变量发生在写入volatile变量之后的情况不做要求,允许其被重排序到写入volatile变量之前。允许从后到前,但不允许从前到后。
-
读取volatile变量之后发生的对其他变量读取的操作不能被重排序到对volatile变量之前发生。注意,发生在读取volatile变量之前的操作可能被重排序到之后发生。允许从前到后,但不允许从后到前
上述happens-before保证确保volatile关键字的可见性保证被强制执行。
volatile关键字的不足
尽管volatile保证了所有对volatile变量的读取是从主存中读取、写入会被直接写入主存,但在一些场景中volatile关键字仍有不足。
在之前的例子中,只有线程A对共享的counter变量进行写入,将counter声明成volatile就足够保证线程B读取到最新值了。
事实上,如果volatile变量的值不依赖之前的值,当多线程写入共享的volatile变量时,仍然会将正确的值写入到主存中。换句话说,如果一个线程将一个值写入共享volatile变量,则不需要首先读取它的值来找出它的下一个值。
但是一旦当线程先读取volatile的值,再将根据旧值生成的新值写入volatile变量时,volatile变量不再足以保证正确的可见性。当多线程读取同一个volatile变量的值,生成新值,然后写入到主存中是,读取volatile变量和写入volatile变量间短暂的时间差会造成竞争条件,彼此覆盖他们的值。
想象一下,线程A读取了一个共享的counter变量的值0,写入CPU的缓存中,然后将它+1操作,但没有将其写入主存中。线程B读取了主存中同一个counter变量的值,此时主存中的值还为0,放入线程B对应的CPU中,线程B也对变量值进行了自增1的操作,也没有将其值写入主存。下图说明了此时的状态。
线程 1 和线程 2 现在几乎不同步。共享counter变量的实际值应该是 2,但每个线程在其 CPU 缓存中的变量值为 1,而在主内存中该值仍为 0。这是一团糟!即使线程最终将共享counter变量的值写回主内存,该值也会是错误的。
volatile何时是足够的?
两个线程同时读取和写入共享变量,那么使用 volatile
关键字是不够的。 在这种情况下,您需要使用synchronized
来保证变量的读写是原子的。读取或写入 volatile 变量不会阻止线程读取或写入。为此,您必须synchronized
在关键部分周围使用关键字。
作为synchronized
块的一种替代,你也可以使用在java.util.concurrent
中许多原子数据类型中的一个。例如,AtomicLong或AtomicReference或其他。
如果只有一个线程读取和写入 volatile 变量的值而其他线程只读取该变量,则保证读取线程看到写入 volatile 变量的最新值。如果不使变量可变,则无法保证这一点。
volatile
关键字保证适用于 32 位和 64 位变量。虽然对于非volatile
的64位变量,会分为两次写入,一次写入一半,也就是32位。但对于volatile
的long和double类型的写入被保障为原子的,详见官方文档
性能考虑
volatile
变量的读取和写入导致变量被读取或写入主存储器。读取和写入主内存比访问 CPU 缓存更昂贵。访问volatile
变量还可以防止指令重新排序,这是一种正常的性能增强技术。因此,应该只在确实需要强制执行变量可见性时才使用 volatile
变量。