• 在多处理器开发中保证共享变量的可见性。

  • 比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

  • 计算机存储体系中,自顶向下:CPU寄存器 -> CPU缓存(L1、L2、L3) -> 内存 -> 其他存储设备,如硬盘。

  • 使用volatile关键字修饰的变量,在多核处理器下会引发两件事情:

    • 将当前处理器缓存的数据写回到系统内存。
    • 这个写回内存的操作会使其他CPU里缓存了该内存的数据无效(缓存一致性协议,即每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,过期则在使用该变量时重新从内存读取该数据到该处理器缓存中)

    (Java并发编程的艺术)

  • 关于volatile变量的可见性,经常被人误解:“volatile变量对所有线程是立即可见的,换句话说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是线程安全的。”这句话论据部分没有问题,但是这样下结论是错的。

    例如,Java里面的运算操作并非原子操作,这就导致了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
// volatile变量自增运算测试

public class VolatileTest {

public static volatile int race = 0;

public static void increase() {
race++;
}

private static final int THREADS_COUNT = 20;

public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for(int i = 0; i < THREADS_COUNT; ++i) {
threads[i] = new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 10000; ++i) {
increase();
}
}
});
}
}

// 等待所有累加线程都结束
while(Thread.activateCount() > 1) {
Thread.yield();
}

System.out.println(race);
}

执行后可发现,输出结果并非200000,问题出在"race++"这里,因为只有一行代码的increase()方法在Class文件中是由4条字节码指令构成的

1
2
3
4
5
6
7
8
9
10
11
public static void increase();
Code:
Stack=2, Locals=0, Args_size=0
0: getstatic #13; //Field race:I
3: iconst_1
4: iadd
5: putstatic #13; //Field race:I
8: return
LineNumberTable:
line 14: 0
line 15: 8

虽然使用字节码并不严谨,但是已经能说明问题。由于volatile变量只能保证可见性,在类似上述场景中,我们仍应加锁(synchronized、java.util.concurrent中的锁或者原子类)来保证原子性。

不过,类似下面这种场景就很适合volatile变量来控制并发,当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都立即停下来。

1
2
3
4
5
6
7
8
9
10
11
volatile boolean shutdownRequested;

public void shutdown() {
shutdownRequested = true;
}

public void doWork() {
while(!shutdownRequested) {
// 其他代码逻辑
}
}

使用volatile变量的第二个语义是禁止重排序优化(即“有序性”),普通的变量仅会保证该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序和程序代码中的执行顺序一致。因为在同一个线程的方法执行过程中无法感知到这点,这就是Java内存模型中描述的所谓“线程内表现为串行”的语义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Map configOptions;
char[] configText;
// 此变量必须定义为volatile
volatile boolean initialized = false;

// 假设这段代码在线程A执行
// 模拟读取配置信息,当读取完成后
// 将initialized变为true,通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized = true;

// 假设以下代码在线程B中执行
// 等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized){
sleep();
}
// 使用线程A中初始化好的配置信息
doSomethingWithConfig();

如果initialized变量不使用volatile修饰,则可能发生指令重排,导致线程A中最后一行代码"initialized = true"被提前执行,这样线程B中使用配置信息的代码就会存在问题,因此需要volatile关键字来避免类似的情况。

类似的例子还有双锁检测(Double Check Lock, DCL)单例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private volatile static Singleton instance;

public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}

public static void main(String[] args) {
Singleton.getInstance();
}
}

因“instance = new Singelton()”这句话可分为三步:

1.为instance分配内存空间

2.初始化instance

3.将instance指向分配的内存空间

由于jvm可能指令重排为1-3-2,单线程下无问题,但是多线程下会导致一个线程获得一个未初始化的实例。例如,线程T1执行了1和3,此时线程T2调用getInstance()后发现instance不为空,因此返回instance,但此时instance还没有被初始化,所以需使用volatile来禁止指令重排。

参考:《深入理解Java虚拟机》

非原创,有一些参考内容时间久了找不到出处,若有机会后续标明,在此表示感谢!!