java volatile 只能保证可见行, 不能操作保证一致性
前段时间, 有个来面试的Java工程师说: 由volatile修饰的变量相当于synchronized修饰的块, 能保证一致性.
这个错误的结论其实很容易被证明是错误的, 以下代码由两个线程对同一个 volatile 变量进行操作, 一个顺序加100000, 另外一个顺序减100000, 为了防止JVM 对于for loop 进行优化, 每一次循环都打印当前的i变量, 结果有时候不是0.
package com.tianxiaohui.art;
public class Main {
public static volatile int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
System.out.print(i);
count++;
}
}
});
Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
System.out.print(i);
count--;
}
}
});
t2.start();
t1.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
System.out.println(count);
}
}
运行结果:
01234567891001234....
1
为什么会出现这种情况?
使用 volatile 修饰的变量在进行写操作的时候, 会发生两件事情:
- 当前缓存行数据写回主存, 也有书上说当前线程workspace的数据写回主存;
- 写回主存的操作会使其它CPU里缓存该内存地址的缓存无效.
也就是说一旦有一个CPU里的volatile的变量有写操作, 它会立即写回主内存, 并且使其它CPU的缓存里有个变量的值都无效. 效果类似于对于这个volatile 修饰的变量没有缓存, 全部直接使用内存操作.
上面的代码里面对于 volatile 变量的操作 ++ 其实不是一个原子操作, 尽管它能保证每个CPU 拿到count值时候都是最新的值, 但是对它的加1, 再写回内存并不是原子操作. 举例来说, 某时间点CPU-0 和 CPU-1要对它操作之前同时拿到count的值都是5, 然后CPU-0对5加1, 得到6, CPU-1对5减1, 得到4, 那么赋值回去的时候, CPU-0先写回, 主内存值变成6, 同时导致CPU-1里面的值失效, 不过这个时候CPU-1已经在做加操作,之后CPU-1把加的结果4赋值给count, 写回主内存, 变成4. 更细一点讲是一个+操作包含好几条内存指令, 从内存load 它的值只是最早一步.
volatile变量能保证每个线程读到的数据都是主内存最新的值. 一旦有个写操作, 就会导致其它CPU里面的缓存的该值无效, 对它的read都会重新从主内存读取. 但是对它的操作不能保证一致性. 操作一致性要通过同步块或者CAS来保证.
volatile最适合的使用场景是作为一个开关, 一旦有一个线程对这个开关做了操作, 其它线程立马就感知到.