volatile的内存语义 深度理解Java中volatile的内存语义

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

volatile的内存语义 深度理解Java中volatile的内存语义

GDUT_Ember   2021-06-16 我要评论
想了解深度理解Java中volatile的内存语义的相关内容吗,GDUT_Ember在本文为您仔细讲解volatile的内存语义的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:volatile的内存语义,Java内存模型,下面大家一起来学习吧。

volatile可见性实验

举个栗子

在这里插入图片描述

我这里开了两个线程,后面的线程去修改volatile变量,前面的线程不断获取volatile变量,

结果是会一致卡在死循环,控制台没有任何输出

假如将flag让volatile来进行修饰

在这里插入图片描述

结果是:三秒后,就不会不断打印出信息出来

注意,Thread.sleep是会刷新线程内存的,所以不要使用Thread.sleep来分别让一个线程获取两次volatile变量

volatile的特性

volatile其实相当于对变量的单词读或写操作加了锁、做了同步

由于是加了锁,所以就有前面提到的锁的语义,即锁的happens-before,锁的happens-before规定了释放锁的操作对于后续获得锁操作是可见的,所以释放锁的线程对于后续获得锁的线程是可见的,意味着volatile修饰的变量的最后写入是可以被后面获得锁的线程读取的

32位的操作系统去操作64位的变量时,会分成高32位和低32位去执行,但由于锁,会导致这个操作也是具有原子性的,因为锁的语义决定了临界区代码的执行具有原子性,即必须要整个代码块执行完,如果没有锁,那么就不是原子性的,可能会被分成不连续的两步来执行

所以,volatile变量自身是具有下面特性的

1.原子性:无论多大的变量,对其单词读或写操作都是具有原子性的,但如果类似于i++这种操作就不具备原子性了,因为这本来就是两条命令

2.可见性:操作volatile变量的线程是可以获取前一个线程对其的修改,即当前线程总是可以看到volatile变量最后的写入

volatile 写与读的内存语义

我们先来研究一下什么依赖关系需要volatile

前面提到过总共有三种依赖关系

  • 读后写
  • 写后读
  • 写后写

volatile是实现可见性的,所以写后写就不用考虑了,而且读后写是不需要可见性的,所以需要可见性的是写后读

写语义

volatile写的内存语义如下:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存(即不仅修改了本地内存,而且还刷新到了主内存),注意,这个刷新是按缓存行的形式(64字节)

举个栗子

两个线程,A线程修改flag与A,flag与A原本为默认值

在这里插入图片描述

所以volatile的写是有两个操作的,然后这两个操作会合成一个原子操作

读语义

volatile的读内存语义为:当读一个volatile变量时,JVM会把线程对应的本地内存置为无效,接下来重新去主内存中读取共享变量,并且更新本地内存,注意:是读的时候会置为无效,假如不读就不会置为无效然后重新获取

还是上面的栗子,不过多了一个线程B,线程B一开始读的是默认值,后来再进行了一次读取

在这里插入图片描述

总结一下读写语义

读写语义对应的其实就是volatile的变量修饰后,会进行怎样的过程

其实volatile的读写语义,就是线程之间的通信,所以volatile也是实现了线程之间的通信,来提供可见性

  • 线程A去写volatile变量,实质上是线程A对其他要操控该volatile变量的其他线程发出了消息,该消息表明了线程A已经把该变量修改了,其他线程需要重新去获取
  • 线程B去读volatile变量时,实质上是线程B接收到了之前某个线程发出的消息(可能没有消息,不过也认为接收到),知道这个变量改了,需要去重新获取
  • 所以A写B读,就实现了两个线程之间的通信,虽然不太严谨,因为可能A不写,B也要读

volatile的实现

前面已经提到过volatile的实现,字节码上加了acc_volatile修饰符,然后指令层面上是使用了内存屏障,下面就来再详细研究

volatile的内存语义实现

volatile还有一个功能就是可以防止命令重排序,也就是volatile的内存语义

为了实现volatile内存语义,JMM会限制重排序,因为重排序会让语义出现变化,也就是会打断与别的线程的通信,前面提到过,重排序总共有三种,而JMM会限制编译器重排序与处理器重排序,并不会限制内存重排序

单纯看表,很难去辨别为什么,所以下面只看不发生重排序的部分

  • 当第二个操作是volatile写时,无论第一个操作是什么,都不能发生重排序,保证了volatile写之前的操作不会被重排序到写后面
  • 当第一个操作是volatile读的时候,无论第二个操作是什么,都不能发生重排序,保证了volatile读之后的操作不会被重排序到读之前
  • 当第一个操作为volatile写的时候,且第二个操作是volatile读的时候,是不可以发生重排序

第三个比较容易理解,因为volatile写会影响后面volatile读的嘛,先写后读跟线读后写是完全不一样的,所以两次操作分别为volatile读和volatile写或volatile写和volatile读都是不允许重排序的

关键在于前两条怎么理解

其实都是因为volatile的读语义,每次volatile读都会使缓存行失效,需要去重新获取缓存行,缓存行中不仅有volatile变量,还有其他共享变量

现在回到第二条

  • 当第一个操作为volatile读的时候,后面也是普通读,重排序是没有问题,但如果后面是普通写,普通写后续可能是会刷新进主存中的,此时volatile读是会出现问题的
  • 当第一个操作为volatile读的时候,第二个操作也为volatile读的时候,会形成两次新的缓存行,而每次缓存行相同变量对应的值都可能不一样,此时如果发生重排序,就会出现不一致,比如,不发生重排序时,从第一次新的缓存行里面读A,从第二次新的缓存行里面读B,发生了重排序后,就是从第一次新的缓存行里面读B2,从第二次新的缓存行里面读A2,B与B2是不一样的,A于A2也是不一样的,所以不可以重排序

现在回到第一条

  • 当第一个操作为volatile写的时候,会直接修改主存,影响后面的volatile读,所以对于第二个操作为volatile读是不可以重排序的
  • 当第一个操作为volatile写的时候,会直接修改主存,是会对其他线程造成影响的,同时重排序的话,会造成结果不一致,所以也不可以重排序volatile写
  • 当第一个操作为volatile写的时候,可以普通读,但不可以普通写,因为普通写后面也会更新到主存中去,重排序也是会导致结果不一致的

接下来关于不需要重排序

  • 普通读写和普通读写之前没有volatile要求,所以可以重排序,当然这会导致并发问题
  • 普通读写和volatile读之间,只有一个volatile读要求,这个读要求不会被普通读写影响,所以也是可以重排序,不过对于普通读写部分会产生并发问题

为了实现内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,也就是上面提到的限制重排序的类型,对于执行效率来说,屏障数越少越好,但让JMM去动态发现最优的屏障布置是不可能的,所以采用了保守策略的JMM内存屏障和插入策略

1.在每一个volatile写操作的前面插入一个StoreStore屏障,保证了在volatile写操作之前,上面的所有写操作已经执行完成,并且都刷新到主存中

2.在每一个volatile写操作的后面插入一个StoreLoad屏障,保证了必须执行完volatile写操作,下面的读操作才可以执行

3.在每一个volatile读操作的后面插入一个LoadLoad屏障,保证了在volatile读之前,上面的所有读操作都要完成

4.在每一个volatile读操作的后面插入一个LoadStore屏障,保证了下面的写操作,必须要等待volatile读操作完成才可以继续

由于第一次操作为普通读,第二次操作为volatile读是允许发生重排序的,所以volatile读前面不需要加内存屏障

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们