转载自  再有人问你volatile是什么,把这篇文章也发给他

在上一篇文章中,我们围绕volatile关键字做了很多阐述,主要介绍了volatile的用法、原理以及特性。在上一篇文章中,我提到过:volatile只能保证可见性和有序性,无法保证原子性。关于这部分内容,有读者阅读之后表示还是不是很理解,所以我再单独写一篇文章深入分析一下。阅读本文之前,请先阅读上一篇文章:再有人问你volatile是什么,就把这篇文章发给他

volatile与有序性

在上一篇文章中我们提到过:volatile一个强大的功能,那就是他可以禁止指令重排优化。通过禁止指令重排优化,就可以保证代码程序会严格按照代码的先后顺序执行。那么volatile又是如何禁止指令重排的呢?

先给出结论:volatile是通过内存屏障来来禁止指令重排的。

内存屏障(Memory Barrier)是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。下表描述了和volatile有关的指令重排禁止行为:

从上表我们可以看出:

当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

具体实现方式是在编译期生成字节码时,会在指令序列中增加内存屏障来保证,下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。

    • 对于这样的语句Store1; StoreLoad; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

  • 在每个volatile写操作的后面插入一个StoreLoad屏障。

    • 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

  • 在每个volatile读操作的后面插入一个LoadLoad屏障。

    • 对于这样的语句Load1;LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • 在每个volatile读操作的后面插入一个LoadStore屏障。

    • 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

所以,volatile通过在volatile变量的操作前后插入内存屏障的方式,来禁止指令重排,进而保证多线程情况下对共享变量的有序性。

volatile与可见性

在上一篇文章中我们提到过:Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。

其实,volatile对于可见性的实现,内存屏障也起着至关重要的作用。因为内存屏障相当于一个数据同步点,他要保证在这个同步点之后的读写操作必须在这个点之前的读写操作都执行完之后才可以执行。并且在遇到内存屏障的时候,缓存数据会和主存进行同步,或者把缓存数据写入主存、或者从主存把数据读取到缓存。

这里稍微拓展一下,我们在内存模型是怎么解决缓存一致性问题的一文中介绍过缓存一致性协议,同时也提到过内存一致性模型的实现可以通过缓存一致性协议来实现。同时,留了一个问题:已经有了缓存一致性协议,为什么还需要volatile?

这个问题的答案可以从多个方面来回答:

1、并不是所有的硬件架构都提供了相同的一致性保证,Java作为一门跨平台语言,JVM需要提供一个统一的语义。

2、操作系统中的缓存和JVM中线程的本地内存并不是一回事,通常我们可以认为:MESI可以解决缓存层面的可见性问题。使用volatile关键字,可以解决JVM层面的可见性问题。

3、缓存可见性问题的延伸:由于传统的MESI协议的执行成本比较大。所以CPU通过Store Buffer和Invalidate Queue组件来解决,但是由于这两个组件的引入,也导致缓存和主存之间的通信并不是实时的。也就是说,缓存一致性模型只能保证缓存变更可以保证其他缓存也跟着改变,但是不能保证立刻、马上执行。

其实,在计算机内存模型中,也是使用内存屏障来解决缓存的可见性问题的(再次强调:缓存可见性和并发编程中的可见性可以互相类比,但是他们并不是一回事儿)。

写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回主存。读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于Store Buffer和Invalidate Queue的非实时性带来的问题。

所以,内存屏障也是保证可见性的重要手段,操作系统通过内存屏障保证缓存间的可见性,JVM通过给volatile变量加入内存屏障保证线程之间的可见性。

内存屏障

再来总结一下Java中的内存屏障:用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。

volatile与原子性

在以前的文章中,我们介绍synchronized的时候,提到过,为了保证原子性,需要通过字节码指令monitorentermonitorexit,但是volatile和这两个指令之间是没有任何关系的。volatile是不能保证原子性的。

网上有很多文章,拿i++的例子说明volatile不能保证原子性,然后进行各种分析,有的说由于引入内存屏障导致无法保证原子性,有的说一段i++代码,在编译后字节码为:

10: getfield      #2                  // Field i:I
14: iconst_1
15: iadd
16: putfield      #2                  // Field i:I

在不考虑内存屏障的情况下,一个i++指令也包含了四个步骤。

这些分析,只是说明了i++本身并不是一个原子操作,即使使用volatile修饰i,也无法保证他是一个原子操作。并不能解释为什么volatile为啥不能保证原子性。

要我说,由于CPU按照时间片来进行线程调度的,只要是包含多个步骤的操作的执行,天然就是无法保证原子性的。因为这种线程执行,又不像数据库一样可以回滚。如果一个线程要执行的步骤有5步,执行完3步就失去了CPU了,失去后就可能再也不会被调度,这怎么可能保证原子性呢。

为什么synchronized可以保证原子性 ,因为被synchronized修饰的代码片段,在进入之前加了锁,只要他没执行完,其他线程是无法获得锁执行这段代码片段的,就可以保证他内部的代码可以全部被执行。进而保证原子性。

但是synchronized对原子性保证也不绝对,如果真要较真的话,一旦代码运行异常,也没办法回滚。所以呢,在并发编程中,原子性的定义不应该和事务中的原子性一样。他应该定义为:一段代码,或者一个变量的操作,在没有执行完之前,不能被其他线程执行。

那么,为什么volatile不能保证原子性呢?因为他不是锁,他没做任何可以保证原子性的处理。当然就不能保证原子性了。

总结

本文在上一篇文章的基础上,再次介绍了volatile和原子性、有序性以及可见性之间的关系。有序性和可见性是通过内存屏障实现的。而volatile是无法保证原子性的。

再有人问你volatile是什么,把这篇文章也发给他(深入分析)相关推荐

  1. 再有人问你volatile是什么,就把这篇文章发给他

    转载自  再有人问你volatile是什么,就把这篇文章发给他 在再有人问你Java内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题, ...

  2. 再有人问你volatile是什么,把这篇文章也发给他。

    在上一篇文章中,我们围绕volatile关键字做了很多阐述,主要介绍了volatile的用法.原理以及特性.在上一篇文章中,我提到过:volatile只能保证可见性和有序性,无法保证原子性.关于这部分 ...

  3. 再有人问你volatile是什么,就把这篇文章发给他,让他哑口无言

    本文就围绕volatile展开,主要介绍volatile的用法.volatile的原理,以及volatile是如何提供可见性和有序性保障的等. volatile这个关键字,不仅仅在Java语言中有,在 ...

  4. 再有人问你synchronized是什么,就把这篇文章发给他。

    在<深入理解Java虚拟机>中,有这样一段话: synchronized关键字在需要原子性.可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是"万能"的 ...

  5. 毕业5年决定人的一生-- 大家千万不要错过这篇文章

    毕业5年决定人的一生-- 大家千万不要错过这篇文章 大家千万不要错过这篇文章,毕业三年多了,能看到这篇文章也是一种幸运,真的受益匪浅,对我有很大启迪,这篇文章将会改变我的一生,真的太好了,希望与有缘人 ...

  6. 再有人问你Java内存模型是什么,就把这篇文章发给他

    前几天,发了一篇文章,介绍了一下JVM内存结构.Java内存模型以及Java对象模型之间的区别.有很多小伙伴反馈希望可以深入的讲解下每个知识点.Java内存模型,是这三个知识点当中最晦涩难懂的一个,而 ...

  7. 再有人问你MySQL索引原理,就把这篇文章甩给他!

    △Hollis, 一个对Coding有着独特追求的人△ 这是Hollis的第 335 篇原创分享 作者 l zyz1992 来源 l Hollis(ID:hollischuang) 索引,可能让好很多 ...

  8. 再有人问你Java内存模型是什么,就把这篇文章发给他。

    前几天,发了一篇文章,介绍了一下JVM内存结构.Java内存模型以及Java对象模型之间的区别.有很多小伙伴反馈希望可以深入的讲解下每个知识点.Java内存模型,是这三个知识点当中最晦涩难懂的一个,而 ...

  9. redisson的锁的类型_再有人问你分布式锁是什么,就把这个丢给他!

    [小宅按]现在面试都会聊聊分布式系统,通常面试官都会从服务框架(Spring Cloud.Dubbo),一路聊到分布式事务.分布式锁.ZooKeeper 等知识.今天就来聊聊分布式锁这块的知识,先具体 ...

最新文章

  1. 僵尸(bot)程序缓解
  2. 2019年最受欢迎的JVM配置参数是怎样的?
  3. SQL Server 2008 缩小数据库日志
  4. Spring-AOP 基于Schema配置切面
  5. 6.神操作(把master上的三个安装包scp给slave)—Hadoop完全分布式搭建完成
  6. SAP MM/FI 自动过账实现 OBYC 接口执行
  7. 在VI中删除行尾的换行符
  8. mysql 日志节点恢复_基于binlog二进制日志的MySQL恢复笔记
  9. Gym 100553J Jokewithpermutation(dfs)
  10. java的响应机制_JAVA事件响应机制
  11. 浅析IRF虚拟化技术增强企业网络架构的弹性
  12. php 取消命名空间,到PHP命名空间或不到PHP命名空间
  13. [poj] 1236 networks of schools
  14. Python爬虫实战+数据分析+数据可视化(汽车之家)
  15. js传参中文格式不对乱码
  16. 1044: 不及格率 Python
  17. i9023 中国联通 3G 西安信息中心号码
  18. core 读取视图html,ASP.NET Core MVC 之视图(Views)
  19. 一次关于Uber的通宵抬杠
  20. python3爬取网易云歌单数据清洗_实例 | 使用网易云音乐数据演示数据整合与数据清洗...

热门文章

  1. 历史版本_新版本爆料第弹丨英雄练习新去处,荣耀历史秀出来!
  2. webpack实战之手写一个loader和plugin
  3. java多线程原子操作_Java 多线程 - 原子操作CAS
  4. [JavaWeb-MySQL]DQL_查询表中记录,语句
  5. [蓝桥杯][2014年第五届真题]兰顿蚂蚁-模拟
  6. 关于堆的判断 (25 分)
  7. 分区式存储管理c++_分区机要变形缝,纵横交接卫浴厨:防火阀参数的高效记忆口诀...
  8. 使用Java查询Sql Server数据库
  9. Redis学习之Docker环境搭建
  10. #2693. jzptab