特性
可见性:对一个volatile的读,总能看到对这个volatile变量的最后写入
原子性:对于一个bolatile变量的单个读/写操作具有原子性,但类似于voliatile++这种复合操作不具有原子性
概念
volatile是如何来保证可见性的呢?让我们在X86处理器下通过工具获取JIT编译器生成的 汇编指令来查看对volatile进行写操作时,CPU会做什么事情。 Java代码如下。
instance = new Singleton(); // instance是volatile变量
转变成汇编代码,如下。
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查IA-32
架构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情。
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)
后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile
的 变量进行写操作,JVM就会向处理器发送一条Lock
前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。
所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当 处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状 态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存 里。
MESI 含义
M:代表已修改(Modified):Cache Block 里面的内容我们已经更新过了,但是还没有写回到主内存里面;
E:代表独占(Exclusive):Cache Block 里面的数据和主内存里面的数据是一致的;
S:代表共享(Shared):Cache Block 里面的数据和主内存里面的数据是一致的;
I:代表已失效(Invalidated):Cache Block 里面的数据已经失效了,不可以相信这个 Cache Block 里面的数据;
重点是E和S
- 在独占状态下,对应的 Cache Line 只加载到了当前 CPU 核所拥有的 Cache 里。其他的 CPU 核,并没有加载对应的数据到自己的 Cache 里。这个时候,如果要向独占的 Cache Block 写入数据,我们可以自由地写入数据,而不需要告知其他 CPU 核;
- 在独占状态下的数据,如果收到了一个来自于总线的读取对应缓存的请求,它就会变成共享状态。这个共享状态是因为,这个时候,另外一个 CPU 核心,也把对应的 Cache Block,从内存里面加载到了自己的 Cache 里来;
- 共享状态下,因为同样的数据在多个 CPU 核心的 Cache 里都有。所以,当我们想要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,其他 CPU 核心嗅探到请求后,先把里面的 Cache都变成无效的状态然后返回响应,之后再重新从主存上读取数据;
验证可见性
public class VolatileThread extends Thread {
// 1.能够保证可见 当一个线程在修改我们主内存共享变量的数据 对另外一个线程可见
private volatile static boolean flag = true;
@Override
public void run() {
while (flag) {
}
System.out.println("当前线程停止..");
}
public static void main(String[] args) {
// 默认的情况下创建的线程为用户线程
new VolatileThread().start();
try {
Thread.sleep(1000);
} catch (Exception ignore) {}
flag = false;
System.out.println("主线程停止.");
}
}
运行结果:如果没有volatile关键字,不会跳出while循环,也就是说对于新开启的线程,感知不到flag的改变。当添加volatile关键字后,对flag进行修改时,新开启的线程会基于MESI协议重新从主存上读取flag最新的值,并跳出while循环。
思考:既然同一个线程会被多个核心数执行,那为什么不加volatile的情况下不会跳出while循环?总有一个核心数缓存了最新的数据。
答:因为栈中的栈帧有一个程序计数器,会保存上个CPU切换前的状态,下个cpu核心来执行当前线程的时候,首先会从计数器中读取到上下文到cacheLine,进行恢复现场。
验证i++非原子性
i++在cpu层面实际可以拆分成三个操作:
- 从主存读取i的值到缓存中
- 在缓存中对i进行+1计算
- 将计算后的值刷新到主存中
public class Atomicity {
private static volatile int i = 0;
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 1000; j++) {
new Thread(() -> i++).start();
}
for (int j = 0; j < 1000; j++) {
new Thread(() -> i++).start();
}
Thread.sleep(2000L);
System.out.println(i);
}
}
运行结果:概率性小于等于2000
解析可能出现的情况:
比如线程A和线程B同时对i进行操作,线程A先读取到了i的值到缓存,并在缓存中进行了+1计算,由于Lock指令会立即刷到主存并通知其他CPU核心废弃缓存中的i值,此时线程B又从主存上面读取到缓存的i=1,接着B也做了+1运算,两个线程的结果都为2,再从缓存刷到主存i=2。