概念
JMM 规范解决了线程安全的问题, 主要三个方面: 原子性, 可见性, 有序性, 借助于 synchronized 关键字体现, 可以有效地保障线程安全(前提是你正确运用)
之前说过, 这三个特性并不一定需要全部同时达到, 在有些场景, 部分达成也能够做到线程安全.
volatile 就是这样一个存在, 对可见性和有序性进行保障
可见性
volatile 字面意思, 易变的, 不稳定的, 在 Java 中含义也是如此
想要保证可见性, 就要保障一个线程对于数据的操作, 能够及时的对其他线程可见
volatile 会通知底层, 指示这个变量读取时, 不要通过本地缓存, 而是直接去主存中读取(或者说本地内存失效, 必须去主存读取), 这样如果一个线程对于数据完成写入到主存, 另外线程进行读取时, 就可以第一时间读取到新值, 而非旧值, 所以所谓不稳定, 就是指可能会被其他线程同时并发修改, 所以你要去主存中去重新读取.
他会让写线程冲刷写缓存, 读线程刷新读缓存, 简言之就是操作后立刻会刷新数据, 读取前也会刷新数据;
以保证最新值可以及时更新到主存以及读线程及时的读取到最新值.
注意:
如果 Reader 对于这个共享变量 x 的读取操作有很多个步骤, 比如 x=1;y=x;y=y+1;y=y+2; 等等 最后 x=y;, 如果没有原子性保障, 很显然, 如果已经执行过了 y=x; 再往后的操作过程中, 如果 x 的值再次被改变了, 此时 Reader 中的 y 是无法改变的, 这就出现问题了
所以此处的可见性要注意区分, 在某些场景想要线程安全的话, 可见性对原子性是有依赖的
可见性指的是在你需要的时刻, 如果被别人修改了, 重新读取新的, 但是如果你用过了, 单纯的可见性并不能保证后续没问题.
有序性
volatile 关键字将会直接禁止 JVM 和处理器对关键字修饰的指令重排序, 但是对于 volatile 关键字修饰的前后的, 无依赖的指令, 可以进行重排序
被 volatile 修饰的变量, 可以认为插入了一个内存屏障, 他会进行如下保障:
确保指令重排序时不会将其后面的代码排到内存屏障之前
确保指令重排序时不会将其前面的代码排到内存屏障之后
确保在执行到内存屏障修饰的指令时前面的代码全部执行完成
强制将线程工作内存中值的修改刷新至主内存中
如果是写操作, 则会导致其他线程工作内存 (CPU Cache) 中的缓存数据失效
比如
- int x = 0;
- int y = 1;
- volatile int z=20;
- x++;
- y--;
在语句 volatile int z=20 之前, 先执行 x 的定义还是先执行 y 的定义, 我们并不关心, 只要能够百分之百地保证在执行到 z=20 的时候 x=0, y=1, 同理关于 x 的自增以及 y 的自减操作都必须在 z=20 以后才能发生. 这个结果就是上面的逻辑处理后的结果.
综上所述, volatile 可以对可见性以及有序性进行保障.
那么 volatile 的原子性如何?
原子性
如下面示例, 共享变量 count 是 volatile 的, 在 add 方法中, 对他进行自增, 运行几次后分别查看结果
- package test1;
- public class T12 {
- public static volatile int count = 0;
- public static void add() {
- count++;
- }
- public static void main(String[] args) {
- // 创建 10 个线程, 每个线程循环 1000 次, 最终结果应该是 10,000
- for (int i = 0; i <10; i++) {
- new Thread(() -> {
- for (int j = 0; j <1000; j++) {
- add();
- }
- }).start();
- }
- // 确认其他线程都结束了, 否则不继续执行(确认当前线程组以及子线程组活动线程的个数, JDK8 中这个值设置为 2), 后续有更好的方法完成等待
- while (Thread.activeCount()> 2) {
- Thread.yield();
- }
- System.out.println("count:" + count);
- }
- }
10 个线程, 每个线程 1000 次循环, 按理来说最终的结果应该是 1000
从结果可以看得出来, 并不是线程安全的, 但是既然 volatile 保障了可见性与有序性, 可以推断出来并没有做到原子性
问题出在哪里?
关键在于 count++; 自增操作, 并不是直接的赋值操作, 比如 x=1;
他可以简单的理解为三个步骤:
读取 count 的值;
操作 count 的值;
回写 count 的值;
volatile 可以保障在第一步的时候, 读取到了正确的值, 但是由于不是原子的, 在接下来的操作过程中, count 的值, 可能已经被更新过了, 也就是读取到了旧值
继续使用这个旧值很显然就把别人的更新抹掉了, 你读取的 1, 可能此时应该是 2 了, 但是你操作后还是 2, 无故的擦除了别人的增加, 所以结果才会出现小于 10000 的情况
因为是自增操作, 所以使用旧值会导致小于 10000
如果把初始值设置为 10000, 使用自减 count--, 使用旧值就可能会导致别人的减量被擦除了, 最终大于 0, 不妨修改为自减运算试一下
从结果看得出来, 我们的推断没错, 就是使用了旧值
这就是前面说到的线程安全, 单纯的依赖可见性是不能保障的, 还需要依赖原子性
因为在第一步的时候, 尽管获取到的值肯定是最新的, 但是接下来的过程中呢?
值仍旧可能被改变, 因为并不是原子的
比如, 装着饮料的瓶子, 你从其中取饮料
可见性可以保障你要倒饮料的时候, 瓶子里面是可乐你到出来的是可乐, 装的是雪碧, 倒出来就是雪碧, 但是如果你把可乐倒进自己的杯子里面了, 瓶子瞬间换成雪碧, 你杯子里面的可乐会变化吗?
回想下之前设计模式中介绍过的单例模式, 有一种实现方式是双重检查法
- public class LazySingleton {
- private LazySingleton() {
- }
- private static volatile LazySingleton singleton = null;
- public static LazySingleton getInstance() {
- if (singleton == null) {
- synchronized (LazySingleton.class) {
- if (singleton == null) {
- singleton = new LazySingleton();
- }
- }
- }
- return singleton;
- }
- }
注意:
private static volatile LazySingleton singleton = null;
使用 volatile 修饰
因为实例创建语句: singleton = new LazySingleton(); , 就不是一个原子操作
他可能需要下面三个步骤
分配对象需要的内存空间
将 singleton 指向分配的内存空间
调用构造函数来初始化对象
计算机为了提高执行效率, 会做的一些优化, 在不影响最终结果的情况下, 可能会对一些语句的执行顺序进行调整
也就是上面三个步骤的顺序是不能够保证唯一的
如果先分配对象需要的内存, 然后将 singleton 指向分配的内存空间, 最后调用构造方法初始化的话
假如当 singleton 指向分配的内存空间后, 此时被另外线程抢占(由于不是原子操作所以可能被中间抢占)
线程 2 此时执行到第一个 if (singleton == null)
此时不为空, 那么不需要等待线程 1 结束, 直接返回 singleton 了
显然, 此时的 singleton 都还没有完全初始化, 就被拿出去使用了
根本问题就在于写操作未结束, 就进行了读操作
重排序导致了线程的安全问题
此时可以给 singleton 的声明加上 volatile 关键字, 以保障有序性
上面的两个示例, 看起来都是没有保障原子性, 但是为什么一个使用 volatile 修饰就可以, 而另外一个则不行?
对于 count++, 运算结果的正确性依赖 count 当前的值本身, 而且可能存在多个线程对他进行修改, 而 singleton 则不依赖, 而且也不会多个线程进行修改
所以说, volatile 的使用要看具体的场景, 这也是为什么被称之为轻量级的 synchronized 的原因, 他不能从原子性, 可见性, 有序性三个角度进行保障.
所以从上面这些点也可以看得出来, volatile 并不能替代 synchronized, 很关键的一个点就是他并不能保障原子性
volatile 与 synchronized 对比
总结
volatile 是一种轻量级的同步方式(轻量级的 synchronized, 也就是阉割版的 synchronized)
抛开性能的角度看, synchronized 的正确使用可以百分百解决同步问题, 但是 volatile 却并不能完全解决同步问题, 因为他缺乏一个很重要的保障 --- 原子性
原子性能够保障不可分割, 一旦不能对原子性进行保障, 一旦一个变量的修改依赖自身, 比如 i++, 也就是 i=i+1; 依赖自身的值, 一旦再多线程环境中, 仍旧可能会出错
所以如果换一个思路理解的话, 可以这样:
对于线程安全问题, 主要是三个方面, 原子性, 可见性, 有序性, 不过并不一定所有的场景都需要三者完全保障;
对于 synchronized 关键字都进行了保障, 可以用于线程安全的同步问题
对于 volatile, 他对可见性和有序性进行了保障, 所以如果在有些场景下, 如果仅仅保障了这两者就可以达到线程安全, 那么 volatile 也可以用于线程的同步
所以说 synchronized 可以用于同步, volatile 可以用于部分场景的线程同步
刚才提到对于 i++, 仅仅借助于 volatile, 他相当于 i=i+1, 依赖自身的值的内容, 所以多线程会出问题, 如果只有一个线程才会执行这个操作就不会出现问题
另外, 如果对于一个操作, 比如 i=j+1;j 也是一个共享变量, 很显然多线程场景下, 仍旧可能出现问题
所以如果你使用 volatile 保障线程安全, 需要非常慎重, 必要的时候, 仍旧需要借助于 synchronized 关键字进行同步, 进一步对原子性进行保障.
来源: https://www.cnblogs.com/noteless/p/10410368.html