【死磕Java并发】—–深入分析volatile的实现原理

技术【死磕Java并发】—–深入分析volatile的实现原理 【死磕Java并发】—–深入分析volatile的实现原理通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做

【卡在Java并发上】-深入分析volatile的实现原理。

在前一章中,我们了解到synchronized是一个重量级的锁,尽管JVM已经对它进行了许多优化,而下面描述的volatile是轻量级的synchronized。如果一个变量使用volatile,它比synchronized便宜,因为它不会导致线程上下文的切换和调度。Java语言规范对volatile的定义如下:

Java编程语言允许线程访问共享变量。为了确保共享变量能够被准确和一致地更新,线程应该确保这个变量是通过排他锁单独获得的。

以上有点绕弯。通俗地说,就是说如果一个变量用volatile修饰,那么Java可以保证所有线程看到这个变量的值都是一样的。如果一个线程更新了用volatile修饰的共享变量,其他线程可以立即看到这个更新,这被称为线程可见性。

虽然volatile看起来很简单,只能通过在变量前面加volatile来使用,但是要用好并不容易(LZ承认我还是用不好,用的时候还是模棱两可)。

内存模型相关概念

理解volatile其实有点难。它与Java的内存模型有关,所以在理解volatile之前,我们需要先了解一下Java内存模型的概念。这里我们只做一个初步的介绍,后面LZ会详细介绍Java内存模型。

操作系统语义

当一台计算机运行一个程序时,每一条指令都在CPU中执行,这不可避免地涉及到数据的读写。我们知道程序的运行数据存储在主存中,所以会出现问题。在主存中读写数据不如在CPU中执行指令快。如果任何交互都需要处理主存,会大大影响效率,所以有了CPU缓存。中央处理器缓存对于中央处理器是唯一的,并且只与在该中央处理器中运行的线程相关。

CPU缓存虽然解决了效率问题,但是会带来一个新的问题:数据一致性。在程序运行期间,运行所需数据的副本将被复制到中央处理器缓存中。运行时,CPU不再处理主存,而是直接从缓存中读写数据,数据运行后才会刷新到主存中。举个简单的例子:

当线程运行这段代码时,它会首先从主内存中读取i(i=1),然后复制一个副本到CPU缓存,然后CPU会执行1 (2)的操作,然后将数据(2)写入到tell缓存中,最后刷新到主内存中。其实单线程没有问题,问题在多线程。如下所示:

如果两个线程A和B都执行这个操作(I),按照我们正常的逻辑思维,主内存中I的值应该等于3,但事实是这样的。分析如下:

两个线程将I (1)的值从主内存读取到各自的缓存中,然后线程A执行1操作并将结果写入缓存,最后将其写入主内存。此时主内存中的I=2,线程B做同样的操作,主内存中的I仍然是=2。所以最后的结果是2,不是3。这种现象就是缓存一致性的问题。

缓存一致性有两种解决方案:

通过向总线添加LOCK#锁

通过高速缓存一致性协议

但是方案一有一个问题,是以排他的方式实现的,就是如果用LOCK#锁定总线,只能运行一个CPU,其他CPU都要阻塞,效率比较低。

第二种方案,缓存一致性协议(MESI协议),确保每个缓存中使用的共享变量的副本是一致的。核心思想是这样的:当一个CPU在写数据时,如果发现被操纵变量是共享变量,就会通知其他CPU该变量的缓存行无效,所以其他CPU在读取该变量并发现无效时,会从主存重新加载数据。

Java内存模型

从操作系统层面,上面解释了如何保证数据一致性。接下来我们来看看Java内存模型,稍微研究一下Java内存模型为我们提供了什么保障,在做多线程编程时,Java提供了什么方法和机制来保证程序执行的正确性。

在并发编程中,我们通常会遇到这三个基本概念:原子性、可见性和顺序。我们来看看volatile。

原子性

原子性:即一个操作或多个操作要么全部执行,执行过程不会被任何因素打断,要么不执行。

原子就像数据库中的事务。他们是一个团队,生死与共。其实理解原子性很简单。让我们看看下面这个简单的例子:

I=0;- 1

j=I;- 2

我;- 3

I=j 1;- 4

以上四种操作哪一种是原子操作,哪一种不是?如果你不太了解它们,你可能会认为它们都是原子操作。其实只有1个是原子操作,其余都不是。

1-在Java中,基本数据类型的变量和赋值操作是原子操作;

2-包括

两个操作:读取i,将i值赋值给j

  • 3---包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;

  • 4---同三一样

  • 在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作,如long、double*)。要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。

    volatile是无法保证复合操作的原子性

    可见性

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    在上面已经分析了,在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。

    Java提供了volatile来保证可见性。

    当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。当然,synchronize和锁都可以保证可见性。

    有序性

    有序性:即程序执行的顺序按照代码的先后顺序执行。

    在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。

    Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。这里LZ就不再阐述了。

    剖析volatile原理

    JMM比较庞大,不是上面一点点就能够阐述的。上面简单地介绍都是为了volatile做铺垫的。

    volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。

    上面那段话,有两层语义

    1. 保证可见性、不保证原子性

    2. 禁止指令重排序

    第一层语义就不做介绍了,下面重点介绍指令重排序。

    在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序:

    1. 编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;

    2. 处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

    指令重排序对单线程没有什么影响,他不会影响程序的运行结果,但是会影响多线程的正确性。既然指令重排序会影响到多线程执行的正确性,那么我们就需要禁止重排序。那么JVM是如何禁止重排序的呢这个问题稍后回答,我们先看另一个原则happens-before,happen-before原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从happens-before原则中推到出来,那么他们就不能保证有序性,可以随意进行重排序。其定义如下:

    1. 同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。

    2. 监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)

    3. 对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)

    4. 线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)

    5. 线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。

    6. 如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

    我们着重看第三点volatile规则:对volatile变量的写操作 happen-before 后续的读操作。为了实现volatile内存语义,JMM会重排序,其规则如下:

    对happen-before原则有了稍微的了解,我们再来回答这个问题JVM是如何禁止重排序的

    观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。下图是完成上述规则所需要的内存屏障:

    volatile暂且下分析到这里,JMM体系较为庞大,不是三言两语能够说清楚的,后面会结合JMM再一次对volatile深入分析。

    总结

    volatile看起来简单,但是要想理解它还是比较难的,这里只是对其进行基本的了解。volatile相对于synchronized稍微轻量些,在某些场合它可以替代synchronized,但是又不能完全取代synchronized,只有在某些场合才能够使用volatile。使用它必须满足如下两个条件:

    1. 对变量的写操作不依赖当前值;

    2. 该变量没有包含在具有其他变量的不变式中。

    volatile经常用于两个两个场景:状态标记两、double check

    参考资料

    1. 周志明:《深入理解Java虚拟机》

    2. 方腾飞:《Java并发编程的艺术》

    3. Java并发编程:volatile关键字解析

    4. Java 并发编程:volatile的使用及其原理

    PS:如果你觉得文章对你有所帮助,别忘了推荐或者分享,因为有你的支持,才是我续写下篇的动力和源泉!

    • 作者:chenssy。一个专注于【死磕 Java】系列创作的男人

      出处:https://www.cnblogs.com/chenssy/p/15690553.html

      作者个人网站:https://www.cmsblogs.com/。专注于 Java 优质系列文章分享,提供一站式 Java 学习资料

      目前死磕系列包括:

      1. 【死磕 Java 并发】:https://www.cmsblogs.com/category/1391296887813967872(已完成)

      2.【死磕 Spring 之 IOC】:https://www.cmsblogs.com/category/1391374860344758272(已完成)

      3.【死磕 Redis】:https://www.cmsblogs.com/category/1391389927996002304(已完成)

      4.【死磕 Java 基础】:https://www.cmsblogs.com/category/1411518540095295488

      5.【死磕 NIO】:https://www.cmsblogs.com/article/1435620402348036096

      本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

    内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/146474.html

    (0)

    相关推荐

    • 简单平面图怎么画,ppt怎么做简单的平面图

      技术简单平面图怎么画,ppt怎么做简单的平面图1/7
      第一步:在PPT2010中,单击“插入”选项卡,在“插图”组中单击“形状”按钮,在弹出的下拉面板中选择“线条”中的“箭头”,在PPT编辑区按着shift键用鼠标横向拖

      生活 2021年10月30日
    • LuoguP7441 「EZEC-7」Erinnerung 题解

      技术LuoguP7441 「EZEC-7」Erinnerung 题解 LuoguP7441 「EZEC-7」Erinnerung 题解LuoguP7441 「EZEC-7」Erinnerung 题解Co

      礼包 2021年12月16日
    • 怎么理解PHP7.2忽略父类方法以及Liskov替换原则相关问题

      技术怎么理解PHP7.2忽略父类方法以及Liskov替换原则相关问题这篇文章主要介绍“怎么理解PHP7.2忽略父类方法以及Liskov替换原则相关问题”,在日常操作中,相信很多人在怎么理解PHP7.2忽略父类方法以及Li

      攻略 2021年11月16日
    • Hibernate中有哪些核心接口

      技术Hibernate中有哪些核心接口小编给大家分享一下Hibernate中有哪些核心接口,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!包括七

      攻略 2021年12月8日
    • 视频服务器为什么推荐美国服务器

      技术视频服务器为什么推荐美国服务器美国视频服务器主要是指用于部署视频直播或者点播的流媒体SDK服务器。美国视频服务器一般采用独享的物理服务器。虚拟机不同,用户可以自行管理所有硬件资源,直接控制服务器的负载,不受于其他用户

      礼包 2021年11月1日
    • 如何解决红杉离线备份的相关问题

      技术Sequoiadb backupOffline相关问题怎么解决这篇文章主要介绍“Sequoiadb backupOffline相关问题怎么解决”,在日常操作中,相信很多人在Sequoiadb backupOfflin

      攻略 2021年12月22日