volatile也是互斥同步的一种实现,不过它非常的轻量级。
volatile有条关键的语义:
- 保证被volatile修饰的变量对所有线程都是可见的
要理解volatile关键字,我们得先从Java的线程模型开始说起。如图所示:
Java内存模型规定了所有字段(这些字段包括实例字段、静态字段等,不包括局部变量、方法参数等,因为这些是线程私有的,并不存在竞争)都存在主内存中,每个线程会 有自己的工作内存,工作内存里保存了线程所使用到的变量在主内存里的副本拷贝,线程对变量的操作只能在工作内存里进行,而不能直接读写主内存,当然不同内存之间也 无法直接访问对方的工作内存,也就是说主内存时线程传值的媒介。
我们来理解第一句话:
保证被volatile修饰的变量对所有线程都是可见的
如何保证可见性?🤔
被volatile修饰的变量在工作内存修改后会被强制写回主内存,其他线程在使用时也会强制从主内存刷新,这样就保证了一致性。
关于“保证被volatile修饰的变量对所有线程都是可见的”,有种常见的错误理解:
错误理解:由于volatile修饰的变量在各个线程里都是一致的,所以基于volatile变量的运算在多线程并发的情况下是安全的。
这句话的前半部分是对的,后半部分却错了,因此它忘记考虑变量的操作是否具有原子性这一问题。
举个栗子
private volatile int start = 0;
private void volatileKeyword() {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
start++;
}
}
};
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
Log.d(TAG, "start = " + start);
}
这段代码启动了10个线程,每次10次自增,按道理最终结果应该是100,但是结果并非如此。
为什么会这样?
仔细看一下start++,它其实并非一个原子操作,简单来看,它有两步:
- 取出start的值,因为有volatile的修饰,这时候的值是正确的。
- 自增,但是自增的时候,别的线程可能已经把start加大了,这种情况下就有可能把较小的start写回主内存中。
所以volatile只能保证可见性,在不符合以下场景下我们依然需要通过加锁来保证原子性:
- 运算结果并不依赖变量当前的值,或者只有单一线程修改变量的值。(要么结果不依赖当前值,要么操作是原子性的,要么只要一个线程修改变量的值)
- 变量不需要与其他状态变量共同参与不变约束