volatile

volatile具有“同步处理”和“对long和double的原子操作”这两种功能。

同步处理

某个线程对volatile字段进行的写操作的结果对其他线程立即可见。换言之,对volatile字段的写入处理并不会被缓存起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 不可能出现无限while循环
public class Main {

public static void main(String[] args) {
Runner runner = new Runner();
runner.start();
runner.shutdown();
}
}

class Runner extends Thread {
private volatile boolean quit = false;

public void run() {
while (!quit) {
}

System.out.println("Done");
}

public void shutdown() {
quit = true;
}
}

volatile字段并非只是不缓存读取和写入。如果线程A向volatile字段写入的值对线程B可见,那么之前向其他字段写入的所有值都对线程B可见。此外,在向volatile字段读取和写入前后不会发生重排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Main {

public static void main(String[] args) {
final Something obj = new Something();

new Thread() {
@Override
public void run() {
obj.write();
}
}.start();

new Thread() {
@Override
public void run() {
obj.read();
}
}.start();
}
}

class Something {
private int x = 0;
private volatile boolean valid = false;

public void write() {
x = 123;
valid = true;
}

public void read() {
if (valid) {
System.out.println(x);
}
}
}

从volatile的使用目的来看,volatile阻止重排序是理所当然的。如代码所示,volatile字段多被用作判断实例是否变为了特定状态的标志。因此,当要确认volatile字段的值是否发生了变化时,必须先确保非volatile的其他字段的值已经被更新了。

kdET5d.png

在进行如下处理时,线程A向x以及valid写入的值对线程B是可见的。

  • 线程A向字段x写值(normal write)
  • 线程A向volatile字段valid写值(volatile write)
  • 线程B读取volatile字段valid的值(volatile read)
  • 线程B读取字段x的值(normal read)

volatile不会进行线程的互斥处理

关于重排序和可见性这两点,volatile的作用与synchronized的作用非常相似。但是,volatile不进行线程的互斥处理。也就是说,访问volatile字段的线程不会进入等待队列。

访问volatile字段会产生性能开销

“向volatile字段写入的值如果对线程B可见,那么之前写入的所有值就都是可见的”是新增加的内容之一。由于增加了这项内容,向volatile字段读取和写入数据的性能开销就增大了很多。实际上,可以认为访问volatile字段与synchronized的处理耗费的时间几乎相同。

对long和double的原子操作

Java规范无法确保对long和double的赋值操作的原子性。但是,即使是long和double的字段,只要它是volatile字段,就可以确保赋值操作的原子性。

java.util.concurrent.atomic包

在java.util.concurrent.atomic包中有AtomicInteger和AtomicLong等用于进行原子操作的类。这些类可以确保值的比较和加减等操作的原子性。可以认为java.util.concurrent.atomic包是volatile字段的一种通用化形式。