为什么volatile保证不了线程安全

为什么volatile保证不了线程安全

首先要知道,想要线程安全必须保证 原子性可见性有序性
首先要了解的是, Volatile 禁止指令重排序(有序性),保证内存可见性问题,对 变量单个操作保证原子性, 那么它为什么不能保证线程安全呢?
💡 下面首先会单独介绍什么是对变量单个操作保证原子性

概念普及

JMM规定了内存主要划分为主内存和工作内存两种。每个线程都有一个自己的工作内存, 对变量的操作都是在缓存中进行的, 然后再将修改后的值返回到主存中,Java内存模型规定了所有的变量都存储在主内存(Main Memory)中 ,每条线程 还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变 量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图:

线程、主内存、工作内存三者的交互关系

一个变量如何从主内存拷贝到工作内存、如何从 工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成:
JMM8种操作

举个小小的例子:

如果我们在代码中对一个对象重新赋值,int race = 0 ,那么随后就要将新的值同步到当前线程的工作内存,然后再从工作内存同步到主内存,这中间会经历Java内存模型中定义的三种操作use→assign→write ,然后可能会触发缓存一致性协议,使其他线程工作内存中的 race 变量全部失效,其他线程下次获取 race 变量值,都要从主内存中重新获取最新值。

但是,对于未使用volatile 修饰的变量而言,use→assign→write,这三个操作并不是原子性的,这也就意味着其并不是线程安全的。假设有thread-1和thread-2两个线程,thread-1和thread-2工作内存中保留着 race 副本,thread-1对 race 进行了重新赋值,但是其最新值还未同步到主内存,此时thread-2线程读取 race 变量,那么就会直接命中其工作内存获取一个旧数据。

而 volatile 变量将read load use 三个原子操作变成一个原子操作;将assign-store-write变成一个原子操作,这也就意味着每次进行赋值操作都会将最新值刷新到工作内存,使其他线程对此变量的工作副本失效,下次读取必须从工作内存中获取最新值。这就是为什么讲对变量单个操作保证原子性。

volatile变量在各个线程的工作内存中是不存在一致性问题的(从物理存储的角度看,各个线程的工作内存中volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看 不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的.

经典案例分析

下面来看一个例子

/**
* volatitle 变量自增运算测试
*/
public class VolatitleTest {
    public static volatile int race = 0;
    public static void increase(){
        race++;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        for (int i =0; i< threads.length;i++){
            threads[i] = new Thread(()->{
                for (int j = 0; j< 10000; j++){
                    increase();
                    System.out.println(race);
                }
            });
            threads[i].start();
        }

        //等待所有累加线程都结束
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(race);
    }
}

这段代码发起了20个线程, 每个线程对 race 变量进行 10000 次自增操作, 如果这段代码能够正确别难过发的话, 最后输出的结果应该是 200000; 但是最终结果都比 200000 要小??? 为什么呢???

问题就在 race++ , 用 Javap 反编译这段代码后会的到代码清单, 发现只有一行代码的 increase() 方法在 Class 文件中由4条字节码指令构成, 从字节码层面上已经很容易分析出并发失败的原因:getstatic指令吧 race 的值取到操作栈顶时候, volatitle关键字保证了 race 的值此时是正确的. 但是在执行 inconst_1, iadd 这些指令的时候, 其他线程可能已经把 race 的值改变了, 而操作栈顶的值就变成了过期的数据, 所以 putstatic指令 执行后, 就可能把较小的race 值同步回主内存之中.

public static void increase();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field race:I, 读取race 到栈顶
         3: iconst_1
         4: iadd
         5: putstatic     #2                  // Field race:I
         8: return

参考

《Java并发编程的艺术》

《深入理解Java虚拟机》

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值