在《Java 并发编程:核心理论》一文中,我们已经提到可见性、有序性及原子性 问题,通常情况下我们可以通过 Synchronized 关键字来解决这些个问题,不过 如果对 Synchonized 原理有了解的话,应该知道 Synchronized 是一个较重量级 的操作,对系统的性能有比较大的影响,所以如果有其他解决方案,我们通常都 避免使用 Synchronized 来解决问题。
而 volatile 关键字就是 Java 中提供的另一种解决可见性有序性问题的方案。对于 原子性,需要强调一点,也是大家容易误解的一点:对 volatile 变量的单次读/ 写操作可保证原子性的,如 long 和 double 类型变量,但是并不能保证 i++这种 操作的原子性,因为本质上 i++是读、写两次操作。 volatile 也是互斥同步的一种实现,不过它非常的轻量级。 volatile 的意义? 线程会一直等待。 可以尝试获得锁,线程可以不用一直等待 锁状态 无法判断 可以判断 锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可) 性能 少量同步 大量同步
-
防止 CPU 指令重排序 volatile 有两条关键的语义: 保证被 volatile 修饰的变量对所有线程都是可见的 禁止进行指令重排序 要理解 volatile 关键字,我们得先从 Java 的线程模型开始说起。如图所示:
image.png
Java 内存模型规定了所有字段(这些字段包括实例字段、静态字段等,不包括局 部变量、方法参数等,因为这些是线程私有的,并不存在竞争)都存在主内存中, 每个线程会 有自己的工作内存,工作内存里保存了线程所使用到的变量在主内 存里的副本拷贝,线程对变量的操作只能在工作内存里进行,而不能直接读写主 内存,当然不同内存之间也 无法直接访问对方的工作内存,也就是说主内存是 线程传值的媒介。
我们来理解第一句话:
保证被 volatile 修饰的变量对所有线程都是可见的 如何保证可见性?
被 volatile 修饰的变量在工作内存修改后会被强制写回主内存,其他线程在使用 时也会强制从主内存刷新,这样就保证了一致性。 关于“保证被 volatile 修饰的变量对所有线程都是可见的”,有种常见的错误理解: - 由于 volatile 修饰的变量在各个线程里都是一致的,所以基于 volatile 变 量的运算在多线程并发的情况下是安全的。 这句话的前半部分是对的,后半部分却错了,因此它忘记考虑变量的操作是否具有原子性这一问题。
举个例子:
private volatile int start = 0;
private void volatile Keyword() {
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);
}
![](https://img.haomeiwen.com/i7044412/db6f8ac13594b2b4.png)
这段代码启动了 10 个线程,每次 10 次自增,按道理最终结果应该是 100,但是 结果并非如此。 为什么会这样?
仔细看一下 start++,它其实并非一个原子操作,简单来看,它有两步:
1、取出 start 的值,因为有 volatile 的修饰,这时候的值是正确的。
2、自增,但是自增的时候,别的线程可能已经把 start 加大了,这种情况下就有 可能把较小的 start 写回主内存中。
所以 volatile 只能保证可见性,在不符合以 下场景下我们依然需要通过加锁来保证原子性:
- 运算结果并不依赖变量当前的值,或者只有单一线程修改变量的值。(要 么结果不依赖当前值,要么操作是原子性的,要么只要一个线程修改变量 的值) - 变量不需要与其他状态变量共同参与不变约束 比方说我们会在线程里加 个 boolean 变量,来判断线程是否停止,这种情况就非常适合使用 volatile。
我们再来理解第二句话。 禁止进行指令重排序 什么是指令重排序?
指令重排序是指指令乱序执行,即在条件允许的情况下直接运行当前有能 力立即执行的后续指令,避开为获取一条指令所需数据而造成的等待,通 过乱序执行的技术提供执行效率。 指令重排序会在被 volatile 修饰的变量的赋值操作前,添加一个内存屏障, 指令重排序时不能把后面的指令重排序移到内存屏障之前的位置。
网友评论