volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与内存模型紧密相关,因此在讲述 volatile关键字之前,我们有必要先去了解与内存模型相关的概念和知识,然后回头再分析volatile关键字的实现原理,最后在给出volatile关键字的使用场景。

文章目录

  • volatile简介
  • volatile用法
  • JAVA内存模型(JMM)
    • 现代计算机的内存模型
    • 本地内存和主内存
  • JAVA内存模型的三大特性
    • 原子性
    • 可见性
    • 有序性
  • volatile变量的特性
    • 保证可见性
    • 禁止指令重排
    • 无法保证原子性
  • volatile原理
    • 内存屏障
  • 本文小结

volatile简介

在 Java 并发编程中,要想保证线程安全,必须要保证三条原则,即:原子性可见性有序性。只要有一条原则没有被保证,就有可能会导致程序运行不正确。volatile关键字被用来保证可见性,即保证共享变量的内存可见性以解决缓存一致性问题。一旦一个共享变量被 volatile关键字修饰,那么就具备了两层语义:内存可见性禁止进行指令重排序。在多线程环境下,volatile关键字主要用于及时感知共享变量的修改,并使得其他线程可以立即得到变量的最新值。

volatile通常被比喻成"轻量级的synchronized",也是Java并发编程中比较重要的一个关键字。和synchronized不同,volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。

volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰后,那么就具备了两层语义:

  • 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说立即可见
  • 禁止进行指令重排序

volatile用法

使用双重锁校验的形式实现单例模式

package cn.wideth.util;class MySingleton{private volatile static MySingleton instance;public static MySingleton getInstance(){        //1if(instance == null){                        //2synchronized(MySingleton.class){        //3if(instance == null){                //4instance = new MySingleton();   //5}}}return instance;                             //6}public static void main(String[] args) {for (int i = 0; i < 100; i++) {new Thread(new Runnable() {public void run() {System.out.println(Thread.currentThread().getName()+" --> "+MySingleton.getInstance().hashCode());}}).start();}}
}

运行结果


结果分析

需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new MySingleton();可以分解为3行伪代码

a. memory = allocate() //分配内存b. ctorInstanc(memory) //初始化对象c. instance = memory //设置instance指向刚分配的地址

上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。


JAVA内存模型(JMM)

JMM:Java内存模型,是java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存,在说Java内存模型之前先来了解下现代计算机的内存模型。

Java内存模型(JavaMemoryModel)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量,存储到内存和从内存中读取变量这样的底层细节


现代计算机的内存模型

其实早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲。

将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence)

在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。


本地内存和主内存

JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量

本地内存和主内存的关系


正是因为这样的机制,才导致了可见性问题的存在。对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。


JAVA内存模型的三大特性

原子性

原子性即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

  1. 基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。
  2. 所有引用reference的赋值操作
  3. java.concurrent.Atomic.* 包中所有类的一切操作

可见性

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

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。


有序性

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

Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。


volatile变量的特性

保证可见性

(1)当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去

(2)这个写会操作会导致其他线程中的volatile变量缓存无效

当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。— 缓存一致性协议(MESI)。


禁止指令重排

(1)重排序操作不会对存在数据依赖关系的操作进行重排序

(2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变,正确同步的多线程执行结果不会被改变。

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。


无法保证原子性

内存屏障是线程安全的,但是内存屏障之前的指令并不是。现在假设有两个线程,线程 1 和线程 2,对变量 i 进行 i++ 操作,i 的初始值为 10 。假设在某一时刻线程 1 将 i 的值 load 取出来,放置到 cpu cache(cpu 高速缓存) 中,然后再将此值放置到寄存器 A 中, A 中的值自增 1(寄存器 A 中保存的是中间值,没有直接修改 i,因此其他线程并不会获取到这个自增 1 的值)。这时线程切换,轮到线程 2 执行。在此时线程 2 也执行同样的操作,获取值 i 的初始值,自增 1 变为 11,然后马上刷入主内存。此时由于线程 2 修改了 i 的值,根据缓存一致性规则(MESI),线程 1 中对应的的 cpu cache 中 i = 10 的值缓存失效,并将重新从主内存中读取,变为 11。此时线程又发生切换,线程 1 执行,线程 1 将自增过后的 A 寄存器值 11 赋值给 cpu 缓存 i(MESI 只对 cpu cache 有效,无法影响寄存器),并写会主存。最终 i 的值为 11。虽然线程 1 和线程 2 都执行了 i++ 操作,但是最终结果 i 值为 11 。这样就出现了线程安全问题。


volatile原理

volatile是怎样实现了?比如一个很简单的Java代码:

private static volatile boolean isOver = false;

在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令(具体的大家可以使用一些工具去看一下,这里我就只把结果说出来)。我们想这个Lock指令肯定有神奇的地方,那么Lock前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影响:

  1. 将当前处理器缓存行的数据写回系统内存;
  2. 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论:

  1. Lock前缀的指令会引起处理器缓存写回内存;
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
  3. 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。


内存屏障

JMM内存屏障分为四类见下图


Java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:


"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:

  1. 在每个volatile写操作的前面插入一个StoreStore屏障;
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障;
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障;
  4. 在每个volatile读操作的后面插入一个LoadStore屏障。

需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。

StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;

StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序

LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序

LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序

下面以两个示意图进行理解,图片摘自相当好的一本书《Java并发编程的艺术》。



本文小结

本文对volatile关键字以及相关知识进行了详细的介绍,volatile可以解决因为多级缓存和指令优化,以及指令重排序带来的线程安全问题。可以保证线程安全的可见性和有序性,解决的办法是缓存一致性协议和内存屏障。

深入理解并发的关键字-volatile相关推荐

  1. 深入理解并发的关键字-synchronized

    我们已经了解了Java内存模型的一些知识,并且已经知道出现线程安全的主要问题来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的内存可见性问题,以及重排序导致的问题,进一步知道了happens ...

  2. 深入理解并发内存模型||JMM与内存屏障||多核并发缓存架构 ||JMM内存模型||volatile 关键字的作用 ||JMM 数据原子操作||JMM缓存不一致的问题

    深入理解并发内存模型||JMM与内存屏障 多核并发缓存架构 JMM内存模型 volatile 关键字的作用 JMM 数据原子操作 JMM缓存不一致的问题

  3. 如何理解 JAVA 中的 volatile 关键字

    如何理解 JAVA 中的 volatile 关键字 最近在重新梳理多线程,同步相关的知识点.关于 volatile 关键字阅读了好多博客文章,发现质量高适合小白的不多,最终找到一篇英文的非常通俗易懂. ...

  4. Java并发编程:volatile关键字解析(转载)

    转自https://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析 v ...

  5. 【Java并发编程:volatile关键字之解析】

    Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 在Java 5之前,volatile是一个备受争议的关键字:因为在程序中使用它往往会导致出人意料的结果.在Java 5之 ...

  6. 并发编程之深入理解JMM并发三大特性volatile

    并发编程之深入理解JMM&并发三大特性&volatile 并发和并行 并发三大特性 可见性 有序性 原子性 Java内存模型(JMM) JMM定义 JMM与硬件内存架构的关系 内存交互 ...

  7. 转载:Java并发编程:volatile关键字解析

    看到一篇写的很细致的文章,感谢作者 作者:Matrix海子 出处:http://www.cnblogs.com/dolphin0520/ 本博客中未标明转载的文章归作者Matrix海子和博客园共有,欢 ...

  8. 深入理解Java中的volatile关键字

    在再有人问你Java内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized ...

  9. 死磕Java并发:深入分析volatile的实现原理

    本文转载自公众号: Java技术驿站 通过前面一章,我们了解到synchronized是一个重量级的锁,虽然JVM对它做了很多优化. 而下面介绍的volatile则是轻量级的synchronized. ...

最新文章

  1. request.getSession(false)到底返回什么
  2. .NET连接SAP系统专题:C#获取RFC中自定义的异常(四)
  3. mybatis中去除多余的前缀或者后缀
  4. wince ./configure
  5. HugeGraphServer 部署安装
  6. Linux全能终端,【MobaXterm】Windows全能终端神器—MobaXterm
  7. 图---邻接矩阵 建立,深度遍历,广度遍历
  8. 看过曹县国际车展,我闯入了魔幻的塞伯坦
  9. 史上最细的FIFO最小深度计算,(大多数笔试题中都会涉及)
  10. 数据结构笔记(三十)-- 查找的基本概念和相关的顺序查找
  11. Activity与Fragment间的通信
  12. 知乎2019新知青年大会:用问题改变世界的方向
  13. 服务器安全(防止被攻击)
  14. Apache Tomcat 文件包含漏洞(CNVD-2020-10487/CVE-2020-1938)
  15. C语言怎么提出大写字母,c语言函数toupper()如何将小写字母转换为大写字母
  16. Electron主进程渲染进程间通信的四种方式
  17. IntelliJ IDEA 之 jdk Language level
  18. 绘制3D海水温盐密度曲面(matplotlib)
  19. 20.02.12Blah数集(队列)
  20. 计算机或与非门原理,计算机逻辑电路中,与或门,或非门,异或非门,异或门的性质,在线等!!!!...

热门文章

  1. squid 简单介绍及代理说明
  2. Centos6.8 搭建Lvs+Keepalived
  3. Android实现ListView(2)
  4. 【186天】黑马程序员27天视频学习笔记【Day15-上】
  5. 初窥wordcloud之老司机带你定制词云图片
  6. 招聘启事的正确阅读方式您知多少?
  7. 哈希表存在的问题及解决方案
  8. python大纲_python学习大纲
  9. Java面试题--HashMap是什么?
  10. singleflight包原理解析