Java volatile 理解

1. 现代CPU Cache结构

Java volatile 理解_第1张图片
多核 CPU Cache 结构
1.1 缓存的主要作用

现代多核CPU为了提升处理速度,都会将需要的数据从内存拷贝到各自的缓存中(L1,L2),然后在各自的缓存中对数据进行操作。

1.2 缓存的作用范围

L1,L2 是CPU中每个核私有的,用于备份各自所需要的数据。L3 Cache是CPU每个核共享的。

1.3 缓存的写入模式

写回(write-back)模式:各个核每次修改自己缓存中的数据后不会立即写回到内存,而是等到一定合适的时间才写回到内存。
直写(write-through)模式:各个核每次修改自己缓存中的数据后会立即写回到内存。

1.4 缓存的一致性

无论哪种写入模式,试想,如果多个核都从内存缓存了一份相同的数据,而此时有一个核将自己缓存中的数据进行了修改,其他核缓存中的数据却依然是修改之前的数据,这就造成了各个核之间缓存数据的不一致,而我们所期望的是各个核缓存之间的数据可以同步,为了达到同步的目的,各个核的缓存需要共同遵守一份协议,保证修改共享数据的时候可以通知其他缓存,这就是CPU的缓存一致性协议。

缓存一致性协议有多种,但是基本上都是基于MESI协议进行扩展的,详细的内容可以查阅相关文档了解。

2. volatile的实现原理

2.1 可见性

可见性,即一个线程修改一个共享变量时,其他线程可以获取到修改后的值。JVM的实现中,volatile共享变量的写操作会向处理器发送一条Lock指令,声言使用直写模式,在CPU缓存一致性协议的保证下实现volitile共享变量的可见性。

简单来讲,多核环境下,对volatile共享变量进行写操作会将缓存的结果直接写回到内存中,在缓存一致性协议下,其他核会对总线上传输的数据一直进行窥探,即监测其他核对缓存数据的操作。因此,volatile共享变量写回内存的操作会被其他核监测到,此时这些核会将自己缓存中的数据设为无效状态,当需要对这个数据进行修改操作的时候会重新从内存中读取,这样就达到了volatile修饰变量的可见性。

2.1 原子性

volatile修饰的变量无论是读还是写操作,都是具备原子性的,可以理解为使用了锁:

volatile int value = 1;
public void set(int value) {
   this.value = value;
}
public void get() {
   return this.value;
}
int value = 1;
public synchronized void set(int value) {
   this.value = value;
}
public synchronized int get() {
   return this.value;
}

这意味着多线程环境下任何一个线程读取到的volatile修饰的共享变量都是当前的最新值,不会存在差异性。
但是有一点需要注意,volatile只是保证了读/写的原子性,复合的volatile操作并不保证原子性,例如:

volatile int value = 1
public void set(int value) {
   this.value = value++;
}

因为value++这个操作可以分为三个步骤,首先,读取value的值,其次,
进行自增操作,最后,将结果写入内存。需要知道这三个操作volatile并不能保证原子性,即只能保证读取到的value是最新值,如果此时该处理器读取value之后由于某些原因阻塞,而此时其他处理器刚好对value进行了修改,这个时候之前的处理器进行计算时还是使用之前读取到的value,这样就造成了错误的处理结果。

你可能感兴趣的