【死磕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)

    相关推荐

    • openwrt怎么设置master模式(openwrt如何设置进入管理页面)

      技术OpenWRT如何实现工作模式开关这篇文章主要介绍了OpenWRT如何实现工作模式开关,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。 DIR-5

      攻略 2021年12月18日
    • 如何快速搭建实用的爬虫管理平台

      技术如何快速搭建实用的爬虫管理平台这篇文章主要讲解了“如何快速搭建实用的爬虫管理平台”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何快速搭建实用的爬虫管理平台”吧!爬虫

      攻略 2021年10月23日
    • php如何读取远程xml文件并转化为数组

      技术php如何读取远程xml文件并转化为数组这篇文章主要讲解了“php如何读取远程xml文件并转化为数组”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“php如何读取远程x

      攻略 2021年10月27日
    • Sun的新Java脚本语言是什么

      技术Sun的新Java脚本语言是什么本篇文章给大家分享的是有关Sun的新Java脚本语言是什么,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。

      攻略 2021年12月2日
    • 大数据复习

      技术大数据复习 大数据复习大数据复习知识点框架1、大数据概述:复习习题集上的例题即可2、Hadoop:注意单机安装和伪分布式安装的区别,以及Hadoop中块的概念及意义!3、HDFS:(1)名称节点的3

      礼包 2021年11月12日
    • 姓田有涵养的男孩名字,男孩帅气有涵养的名字姓王氏

      技术姓田有涵养的男孩名字,男孩帅气有涵养的名字姓王氏帅气有涵养的王氏名字:王嘉佰姓田有涵养的男孩名字、王杰宁、王钦宁、王伦昌、王子嘉、王皓诚、王哲雨、王建宝、王俊南、王博尘、王佰星、王逸林、王钦睿、王棋嘉、王子睿、王尚翔

      生活 2021年10月24日