Java并发关键字Volatile 详解


  • 问题引出:

    1.Volatile是什么?

    2.Volatile有哪些特性?

    3.Volatile每个特性的底层实现原理是什么?

  • 相关内容补充:

  1. 缓存一致性协议:MESI

    ​ 由于计算机储存设备(硬盘等)的读写速度和CPU的计算速度有着几个数量级别的差距,为了不让CPU停下来等待读写,在CPU和存储设备之间加了高速缓存,每个CPU都有自己的高速缓存,而且他们共享同一个主内存区域,当他们都要同步到主内存时,如果每个CPU缓存里的数据都不一样,这时应该以哪个数据为准呢?为了解决这一同步问题,需要各个处理器都遵循一定的协议,比如MSI,MOSI,MESI等,目前用的比较多的就是MESI协议。

​ 注 :缓存一致性协议是在总线上实现的。

MESI是代表了缓存数据的四种状态,分别是Modified、Exclusive、Shared、Invalid:

​ ①M(Modified):被修改的,处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没 有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。

​ ②E(Exclusive):独占的,处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改, 即与内存中一致。

​ ③S(Shared):共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。

​ ④I(Invalid):要么已经不在缓存中,要么它的内容已经过时。为了达到缓存的目的,这种状态 的段将会被忽略。一旦缓存段被标记为失效,那效果就等同于它从来没被加载到缓存中。在 缓存行中有这四种状态的基础上,

<font color='red'>总结:</font>每个处理器通过嗅探在总线上传递的数据来检查自己缓存的数据是否过期,当处理器发现自己缓存行数据对应的内存地址被修改,就会将当前缓存行里的数据设置为无效。当再次需要使用该数据的时候就会去主内存中重新读取数据。
  1. Java内存模型:JMM 

    ​ Java内存模型规定了Java变量(实例字段,静态字段,构成数组对象的元素等,但不包括局部变量和方法参数)存储到内存和从内存中取出的的底层实现细节,这些变量都存储在主(Main Memory)中,(主内存只是虚拟机内存的一部分) 每个线程都有自己的工作内存(Working Memory),工作内存(实际上工作内存并不存在,他只是JMM抽象出来的一个概念)中保存着从主内存读取来的变量副本拷贝,线程对副本的操作(读取,修改,赋值)都要在工作内存中进行,而不能直接在主内存中进行,不同线程之间也不能访问彼此的工作内存。且线程之间变量值的传递需要经过主内存作为第三方中介。

  1. 内存间原子性交互操作:

    ①lock(锁定):作用于主内存上的变量,当一个变量被标识为Lock的时候,表示该变量是线程 独占状态,此时其他线程不可以对该变量进行操作。早前的缓存一致性协议就是这样,但是这 样会导致某个变量被一个线程占用,其他线程不可以对其进行访问,并发就变成了串行,效率 降低,在后来的缓存一致性协议中就抛弃了这种做法。

    ②unlock(解锁):同样是作用于主内存中的变量,使变量从锁定状态释放出来,其他线程才可 以对其操作。

    ③read(读取):读取主内存中的变量,传输到线程的工作内存,等待后续的load操作。

    ④load(加载):加载工作内存中的变量,把其放入工作内存的副本变量中。

    ⑤use(使用):把工作内存中的变量副本值传递给执行引擎,每当虚拟机遇到使用变量值字节码 的时候就会进行此操作。

    ⑥assign(赋值):把一个从执行引擎接收到的数据赋给工作内存的变量,即执行赋值操作。

    ⑦store(存储):把工作内存中经过赋值更新后的值传递到主内存中,为后续write做准备。

    ⑧write(写入):把store操作传递来的值写入主内存,替换之前的值,完成同步更新。

​ 4.JMM并发的特性要求:

​ ①可见性(Visibility):可见性要求是指当一个线程修改了共享变量的值以后,其他线程能够马 上得知这个修改。

​ ②原子性(Atomicity):原子性是指对变量的操作(read,load,assign等上述交互操作)不 可分割不可被打断,每个操作都要完整的执行完成才可以有其他操作进来。且默认对基本数据类 型的访问和读写都是原子性的(64位的long型和double型会有可能被拆分成两个32位进行读写 操作,但是这种概率极低,可以忽略不计。)

​ ③有序性:为了提升效率,编译器会对代码进行乱序优化,而CPU会乱序执行,但是这样的操作 会导致很严重的问题。为了解决这一问题,使用了内存屏障来防止乱序的发生。 这样按照顺序执 行就是有序性。


​ 进入主题


  • Volatile是什么?

    Volatile是轻量级的synchronized锁,所谓轻量级,是因为synchronized使用时会引起线程的上下文切换,使得执行成本更高,效率更低,而Volatile不会有这些问题,效率更高。

  • Volatile特性:

    1.可见性 :Visibility

    ​ ①定义:当一个线程修改了共享变量的值以后,其他线程能够马上得知这个修改。

    ​ ②先看一个例子:

    package Test;
    
    public class VolatileTest {
    public static boolean flag = false;
    public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("主线程等待线程A修改数据~~~");
    while (!flag){}
    System.out.println("主线程发现数据被线程A修改~~~~");
    }
    }).start();
    Thread.sleep(300); new Thread(new Runnable() {
    @Override
    public void run() {
    changeData();
    }
    }).start(); } public static void changeData(){
    flag = true;
    System.out.println("线程A修改完成数据~~~~");
    } }
    // 执行结果:
    等待线程线程A修改数据~~~
    线程A修改完成数据~~~~

    ​ 可以看出,当线程A修改完成数据后,另外一个线程应该要输出主线程发现数据被线程A修改~~~~,但是实际的运行情况是主线程一直处于等待状态。而如果把 public static boolean flag = false;修改为 public static volatile boolean flag = false;,也就是把变量用volatile修饰,此时的执行结果:

    等待线程线程A修改数据~~~
    线程A修改完成数据~~~~
    线程A修改数据完成~~~~

    很明显,线程A修改变量后,主线程也能感知到,使得数据具有可见性,这就是volatile的作用。

    ​ ③volatile可见性底层实现原理:

    ​ 对未加volatile修饰的变量修改时的底层汇编码:

​ 对volatile修饰的变量修改时的底层汇编码:

由底层汇编可知,对volatile修饰的变量修改时,汇编指令前面会多一个lock前缀,这个lock 前缀将会导致下面两件事发生:

​ (1)立即将修改过的数据回写到主内存中,刷新原来的数据。

在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
ps:摘自https://blog.csdn.net/yu280265067/article/details/50986947

​ (2)如果其他处理器缓存了这个被修改过的数据,那回写操作会使他们失效。

IA-32处理器和Intel64位处理器使用MESI(缓存一致性)维护内部缓存和其他处理器缓存的一致性,在多核处理器(多线程)中,处理器和线程使用嗅探技术检测各自缓存中的数据和总线上传递的数据是否一致,如果检测到有其他处理器或线程回写数据,且该数据是共享数据,那么就会强制使其他缓存了该数据的缓存中的数据失效。

2.有序性(禁止指令重排序):Odering

​ (1)指令重排序:为了优化和性能,编译器和处理器经常会对指令做重排序,且分为三种。

​ ①编译器重排序:在不改变单线程程序语义的前提下,重新安排代码执行顺序。

​ ②指令级并行重排序:处理器采用指令级并行技术将多条指令重叠执行,如果数据不存在 依赖,可以改变机器指令执行。

​ ③内存系统重排序:处理器使用缓存和读/写缓冲区,使得加载和存储看上去是乱序执行。

​ 重排序顺序示意图

​ 先看一个例子:

package Test;

public class NoReoder {
private static int a = 0;
private static boolean flag = false; public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
write();
}
}).start(); new Thread(new Runnable() {
@Override
public void run() {
read();
}
}).start();
} public static void write(){
a = 30;
flag = true;
System.out.println("方法一结束~~");
} public static void read(){
if (flag ) {
a = a + 10;
System.out.println(a);
System.out.println("方法二结束~~");
}
}
}

​ 当线程A启动调用了write方法,线程B启动调用read方法时能不能知道a被write方法修改了呢?答案是:不一定!!!

由于write方法中:

a = 30;
flag = true;

这两个操作的数据没有依赖性,所以可能会被重排序为:

flag = true;
a = 30;

这样就会使得read方法先读到flag = true ,而 a 还没修改完,从而使计算结果出错。

为了解决这种问题,在JMM中设计了内存屏障技术:

简单来说,Volatile的有序性就是靠内存屏障来实现,就是把一些操作限制在某些操作之前或者之后,比如将Store操作限制在Load之前,这样就能让其他线程得到的数据是最新的或者需要先写入数据再让其他线程加载数据。


说在最后:

            相关参考,详见《Java并发编程的艺术》一书

​ 本文仅是对个人学习中一些理解的记录,鉴于水平有限或多或少存在错漏或不严谨之处,欢迎各位大神批评指正。码字不易,欢迎转载转发但请标注出处。

​ 希望病毒早点结束,再难的日子里也要坚持学习,新年快乐,最后愿工作在与病毒抗争最前线的医护人员平安打完这场仗,加油!!!!

Java并发关键字Volatile 详解的更多相关文章

  1. Java并发编程--Volatile详解

    摘要      Volatile是Java提供的一种弱同步机制,当一个变量被声明成volatile类型后编译器不会将该变量的操作与其他内存操作进行重排序.在某些场景下使用volatile代替锁可以减少 ...

  2. java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

    原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...

  3. Java 并发 关键字volatile

    Java 并发 关键字volatile @author ixenos volatile只是保证了共享变量的可见性,不保证同步操作的原子性 同步块 和 volatile 关键字机制 synchroniz ...

  4. Java并发—— 关键字volatile解析

    简述 关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,当一个变量定义为volatile,它具有内存可见性以及禁止指令重排序两大特性,为了更好地了解volatile关键字,我们可以 ...

  5. java并发编程 | 线程详解

    个人网站:https://chenmingyu.top/concurrent-thread/ 进程与线程 进程:操作系统在运行一个程序的时候就会为其创建一个进程(比如一个java程序),进程是资源分配 ...

  6. Java并发基础知识点详解

    1.synchronized与Lock区别 父类有synchtonized,子类调用父类的同步方法,是没办法同步的,因为synchronized不是修饰符,不会被继承下来. synchronized ...

  7. Java并发--lock锁详解

    在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方 ...

  8. java并发lock锁详解和使用

    一.synchronized的缺陷 synchronized是java中的一个关键字,也就是说是Java语言内置的特性.那么为什么会出现Lock呢? 在上面一篇文章中,我们了解到如果一个代码块被syn ...

  9. Java并发--ReentrantLock原理详解

    ReentrantLock是什么? ReentrantLock重入锁,递归无阻塞的同步机制,实现了Lock接口: 能够对共享资源重复加锁,即当前线程获取该锁,再次获取不会被阻塞: 支持公平锁和非公平锁 ...

随机推荐

  1. javascript基础之循环

    //while循环 <script type="text/javascript"> i = 1; while (i <= 6) { document.write( ...

  2. Qt3升至Qt4需要注意的几件事项浅谈

    Qt3升至Qt4需要注意的几件事项浅谈 公司以前的项目是用Qt3写的,随着时间的推移慢慢显示出Qt3有多方面的限制,因此先公司决定用Qt4来改写这个项目,并为软件添加新功能,在此背景先编写此文章. 先 ...

  3. 牛客练习赛11 假的字符串 (Trie树+拓扑找环)

    牛客练习赛11 假的字符串 (Trie树+拓扑找环) 链接:https://ac.nowcoder.com/acm/problem/15049 来源:牛客网 给定n个字符串,互不相等,你可以任意指定字 ...

  4. video实现有声音自动播放

    video实现自动播放有声音 需求:老板见人家可以的,我们的也要可以!!! 前端:自动播放,简单... 要实现:鼠标移入视频播放同时有声音,移出让你暂停,,,,, 问题集合 1- 自动播放实现没有声音 ...

  5. pycharm 更改创建文件默认路径

    1.操作 依次找到以下路径修改为自己想要的路径即可:PyCharm——>Settings——>Appearance&Behavior——>System Setting——&g ...

  6. 经典问题(c++/python)素数、杨辉三角(金字塔型)、统计单词数、简单计算器、密码安全程度、凯撒密码加密、汉诺塔 (python课设实验实例)-- biaobiao88

    [编写程序,输人一个大于2的自然数,然后输出小于该数字的所有素数组成的列表.]所谓素数,是指除了1和自身之外没有其他因数的自然数,最小的素数是2,后面依次是3.5.7.11.13... c++代码: ...

  7. Java方法的参数传递是值传递还是引用传递?

    当基本数据类型(Boolean,byte,char,String,int,Long,float,double)作为参数传递时,传递的是实参值的副本,即传的是值,无论在函数中怎么操作这个副本,实参的值是 ...

  8. Prometheus + Grafana 部署说明之「安装」

    说明 在前面的Prometheus学习系列文章里,大致介绍说明了Prometheus和Grafana的一些使用,现在开始介绍如何从头开始部署Prometheus+Grafana,来监控各个相关的指标数 ...

  9. 【题解】[HAOI2018]染色(NTT+容斥/二项式反演)

    [题解][HAOI2018]染色(NTT+容斥/二项式反演) 可以直接写出式子: \[ f(x)={m \choose x}n!{(\dfrac 1 {(Sx)!})}^x(m-x)^{n-Sx}\d ...

  10. Mybatis入门程序编写

    执行原理 入门程序编写 1.pom.xml 文件 <dependencies> <dependency> <groupId>mysql</groupId> ...