记录阅读内存模型(五)-volatile

本人个人公众号,欢迎大家关注:

记录阅读内存模型(五)-volatilevolatile关键字剖析-下篇https://mp.weixin.qq.com/s/tup5QThQOOM2Y9JtXKWa3w

昨天我们在对volatile关键字进行了阐述,对它的一些特性,内存语义的原理进行了剖析,今天我们继续看看剩下的一些知识点。

1 JSR-133为什么增强volatile的内存语义

    在JSR-133之前的旧Java内存模型中,虽然volatile禁止了变量之间的重排序,但是旧的Java内存模型中,是允许volatile变量与普通变量可以进行重排序的。

我们举个例子:

图片

如上图所示操,操作1普通写和操作2volatile写进行了重排序,操作4线程B读取共享的变量这个操作排在了操作1之前,这样子就会导致操作4操作的时候读到的变量可能就获取不到操作1线程A修改的值(理论上是操作1先执行,操作4后执行,操作4应该会获取到操作1执行的结果)。

因此在旧的内存模型中,volatile的写-读没有锁的释放-获取所具有的内存语义。因此,为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量和普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。

下面我们着重分析分析volatile的特性的原理。

2 可见性原理

当我们面试的时候,面试官必问的环节,但是不是单单的只是答出它的特性,面试官应该希望的是能听到它的原理性的知识点,这样子你才能得到面试官的认可,所以我们来看看我们将变量添加volatile关键字修饰之后,打印出字节码,JMM帮我们添加了什么进去。

public class TestVolatile {    private static volatile Integer num;    public static Integer getNum() {        if (num == null) {            num = 0;        }        return num;    }    public static void main(String[] args) {        Integer result = TestVolatile.getNum();        System.out.println(result);    }}

上面代码很简单,就是一个变量被volatile关键修饰,那我们运行将它打印的字节码,看看关键部分:

图片

上面是我运行打印出的字节码结果,我们先来看看途中我圈出来的部分,后面的*putstatic的含义是给一个静态变量设置值,我们看这行的前面,有个lock关键字,就是当写num这volatile变量时,生成字节码的时候,会帮我们添加lock指令,如果我们不添加volatile关键修饰,则不会帮我们添加lock指令,那么lock指令在这里目的是做什么呢?

其实lock指令在多核处理器下会引发下面的事件:将当前处理器的缓存行数据回写到系统的主内存,同时使其他CPU里的缓存了该内存地址的数据置为无效。

那么问题来了,其他CPU又是如何知道我自己的本地内存该变量的内存地址被修改,被置为无效,这个是怎么做到的呢?

其实这里就引入了总线上嗅探机制来实现的。具体执行步骤如下:

  1. 当对加了volatile修饰的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量在缓存行的数据写回到系统内存。

  2. 这时只是写回到系统内存,但其他处理器的缓存行中的数据还是旧的,要使其他处理器缓存行的数据也是新写回的系统内存的数据,就需要实现缓存一致性协议。

  3. 即在一个处理器将自己缓存行的数据写回到系统内存后,其他的每个处理器就会通过嗅探在总线上传播的数据来检查自己缓存的数据是否已过期,当处理器发现自己缓存行对应的内存地址的数据被修改后,就会将自己缓存行缓存的数据设置为无效。

  4. 当处理器要对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到自己的缓存行,重新缓存

3 有序性原理

至于有序性原理,其实比较好理解,就是生成指令的时候插入内存屏障(之前文章也有说过,这里就不在重复叙述了),禁止编译器重排序和处理器重排序,来达到有序性。

4 原子性原理?

如果你在面试中答出了原子性这个特性,我觉得你应该多半有了解或者也有混淆,或者一般只是答出有序性和可见性,原子性这个大多数人不会答出来,但是你答原子性也不是说不对,但是得看前提条件是怎样的,下面我们来探讨下这个原子性,volatile到底有没有帮我们保证。

什么是原子性操作?

原子性操作就是所有的操作要么都成功执行完,要么都不执行。

为什么我会说你答出原子性,不一定说你对,也不一定说你错?

因为volatile帮我们只是保证了一些简单场景场景的原子性,就是所谓的受限制原子性,就是在某些场景,它可以帮我们做到原子性操作。对单个volatile变量的读/写操作都具有原子性,但是对于复杂的操作,就无法保证原子性操作了,比如volatile++,因为这种操作是几个操作指令结合起来才能完成的这个操作,但是单个的volatile变量的读或者赋值只存在一个指令,所以单个volatile变量的读或写它就保证了原子性。

5 volatile++计算不是原子性原理探讨

我们都说volatile关键字只是保证了有序性和可见性,但是原子性没能帮我们保证。

我们先来看看例子:

public class TestVolatile extends Thread{    private static volatile Integer num = 0;    public void run(){        num ++;    }    public static Integer getNum() {        return num ;    }    public static void main(String[] args) throws InterruptedException {        TestVolatile thread1 = new TestVolatile();        TestVolatile thread2 = new TestVolatile();        thread1.start();        thread2.start();        Thread.sleep(1000);        System.out.println("num=" + getNum());    }}

上面代码很简单,就是开启两个线程,run方法中对num进行++,之后主线程睡眠一秒钟,之后打印num值,理论上num值应该是2,确实是这样子吗,我们多运行几次,看看结果:

结果1(这种跟我们的猜想的):

图片

结果2:

图片

运行多次,就会出现结果2,这就说明了volatile关键对于复杂计算场景,无能帮我们保证原子性,就是线程不安全的。至于为啥不是原子性操作,我们下面来一起揭开这谜底。

首先我们要知道,CPU用到的这个值到底经历了哪些步骤?

大家都知道,为了提高处理速度,处理器一般不直接和内存通信,而是先将系统内存的数据读到内部缓存后再进行操作,意思就是将数据从主内存中拷贝到线程本地内存中,之后从线程本地内存中拷贝到主内存中。《深入理解java虚拟机》书中介绍了在java内存模型中,主内存与工作内存之间的具体交互是通过8种操作来完成的,虚拟机保证了这八种操作每一种都是原子的:

  • lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;

  • read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;

  • load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;

  • use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;

  • assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;

  • store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;

  • write(写入):作用于主内存,它把store传送值放到主内存中的变量中。

  • unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

如果要把一个变量从主内存复制到工作内存,就要顺序地执行read和load操作(该过程对应全局变量的读操作);如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作(该过程对应全局变量的写操作)。volatile的对单个读写操作可以看成是原子的,意思read-load,use-assign,write-store这三个操作分别是原子性操作,但是这三个操作组合结合起来之后就不是原子性了,就是当从主内存读取数据到工作内存再到CPU使用这个过程不是原子操作,或者CPU使用完,回写到工作内存,再到主内存的过程操作不是原子性操作,这期间可以插入其它指令的(包括线程的切换),所以volatile不能保证复杂计算原子性,存在线程安全问题。

今天将volatile的特性的原理性梳理了一遍,希望对大家面试的过程中有帮助!

你知道越多,你不知道的越多!我们下期见!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值