最近讨论volatile,产生几个问题并且想明白了,我觉得我对volatile有了一个比较深入的了解,所以对这几个场景进行总结。

解析

volatile

  • volatile 是一个类型修饰符,可以修饰变量。

特性

  • 保证不同线程对volatile修饰的变量进行操作时的可见性,即一个线程修改了这个变量的值对于其他线程来说是立即可见的(可见性);
  • 禁止进行指令重排序;
  • volatile保证对变量单次读/写的原子性,i++ 这种操作不能保证原子性。

特性对应的几个场景(问题)

可见性和禁止重排序

本来想将可见性和重排序拆开讲的,但是考虑了好久发现能单独体现可见性的例子都不好演示。

了解volatile首先要对java内存模型或者CPU有了解,java内存模型本质是对CPU以及内存的统一抽象。

CPU访问内存速度是不如CPU内部操作的速度,所以为了提高性能引进了CPU缓存也就是常见的L1、L2、L3缓存,这些缓存封装在CPU内部速度比外部缓存更快;每个核心先将主内存中的数据加载到各核心的缓存中,这样每次访问就直接访问这个缓存,写也是先更新核心缓存再更新主缓存,但是就有各个核心缓存与主缓存不一致的问题,这里有MESI协议来保证缓存的一致性。

MESI协议在缓存修改是会通知其它核心去失效,这里就存在写核发送失效响应->其它核心接收响应->其它核心ack,这样的流程需要各个核心进行处理,如果核心当前忙碌就会导致性能上的问题,所以又引入了存储缓存(Store Buffe)失效队列(Invalidate Queues)

  • 存储缓存用于提高写性能,写入存储缓存就直接返回;
  • 失效队列用于提高失效端性能,失效请求存入队列立即ack;

通过以上就可以了解到可见性的问题了,写和失效不是直接生效(同步)的。

于是就有了内存屏障,简单来说内存屏障就是清空存储缓存和失效队列,这样就能够使数据及时更新。

可见性场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Vis{
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while(!ready) {
System.out.println("xxxx");
}
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
Thread.sleep(20);
number = 42; // #1
ready = true; // #2
}
}

可见性在这个例子中只能体现出while有滞后,就是ready改为之后循环还执行了一会。

但是这个很难复现,一般测试的时候你的CPU没有这么忙碌,数据修改能快速更新。所以我认为理解就行!

重排序场景

也以上面那个例子,重排序会优化#1和#2代码的顺序这样就会导致循环中虽然退出来了,但是打印的值却不是42.

重排序还有一个典型的场景:单例模式

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

private One(){
}

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

单例类延迟初始化,并发初始化由于synchronized的存在只会有一条线程new,但是new不是一个原子操作,new包含分配内存空间、初始化、引用赋值。由于重排序的作用会导致先引用赋值再初始化,这样其他就会持有引用进行操作但是实际还未初始化完成。

这里就可以将one变量加上volatile禁止重排序来解决。

原子读写

一般类型的变量有没有volatile其实都能保证原子读写,这里主要是将double和long。

内容是直接复制另外一篇【volatile long/double原子读写原理】。

没有添加volatile

32位Java虚拟机x86处理器下对普通long/double型变量写操作的实现

32位Java虚拟机(JIT编译器)执行(动态编译)的效果见上图,可见写long/double是两次操作,普通long/double型变量的读操作使用2条mov指令实现

添加volatile

32位Java虚拟机x86处理器下对volatile long/double型变量写操作的实现

32位Java虚拟机在x86平台下会使用vmovsd这个原子指令来实现volatile修饰的long/double型变量的读写操作。

参考

【Java 并发笔记】volatile 相关整理