volatile的定义

Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。本文将深入分析在硬件层面上Intel处理器是如何实现volatile的,通过深入分析帮助我们正确地使用volatile变量。

操作系统读写数据

在了解volatile原理之前先来了解一下操作系统操作数据的流程。

计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有 CPU 中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了 CPU 高速缓存。CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。

有了 CPU 高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。

在程序运行中,会将运行所需要的数据复制一份到 CPU 高速缓存中,在进行运算时 CPU 不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后,才会将数据刷新到主存中。

举一个简单的例子:

i = i + 1;

当线程运行这段代码时,

  1. 首先会从主存中读取 i 的值( 假设此时 i = 1 ),
  2. 然后复制一份到 CPU 高速缓存中,
  3. 然后 CPU 执行 + 1 的操作(此时 i = 2),
  4. 然后将数据 i = 2 写入到告诉缓存中,
  5. 最后刷新到主存中。

其实这样做在单线程中是没有问题的,有问题的是在多线程中。如下:

(i初始值等于1)假如有两个线程 A、B 都执行这个操作( i++ ),按照我们正常的逻辑思维主存中的i值应该=3 。

但事实是这样么?分析如下:

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

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

通过在总线加 LOCK# 锁的方式和通过缓存一致性协议

  1. 第一种方案存在一个问题,它是采用一种独占的方式来实现的,即总线加 LOCK# 锁的话,只能有一个 CPU 能够运行,其他 CPU 都得阻塞,效率较为低下。
  2. 第二种方案,缓存一致性协议(MESI 协议),它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个 CPU 在写数据时,如果发现操作的变量是共享变量,则会通知其他 CPU 告知该变量的缓存行是无效的,因此其他 CPU 在读取该变量时,发现其无效会重新从主存中加载数据。

CPU保障一致性,volatile实现原理

volatile是如何来保证可见性的呢?让我们在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情。

volatile Singleton instance = new Singleton();                 // instance是volatile变量

转变成汇编代码,如下。

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查IA-32架构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情。

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

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

来具体讲解volatile的两条实现原则。

  1. Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在执行指令期间,声言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存[2]。但是,在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。在8.1.4节有详细说明锁定操作对处理器缓存的影响,对于Intel486和Pentium处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和目前的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。IA-32处理器和Intel 64处理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如,在Pentium和P6 family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

volatile只可以保障可见性,不可保障原子性

在并发编程中我们一般都会遇到这三个基本概念:原子性、可见性、有序性。先了解一下这三个概念,然后再讨论volatile只保障了可见性。

原子性

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

原子性就像数据库里面的事务一样,我们看下面一个简单的例子即可:

i = 0;  // <1>
j = i ; // <2>
i++; // <3>
i = j + 1; // <4>

上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有 1 才是原子操作,其余均不是。

  1. 在 Java 中,对基本数据类型的变量和赋值操作都是原子性操作。
  2. 包含了两个操作:读取 i,将 i 值赋值给 j 。
  3. 包含了三个操作:读取 i 值、i + 1 、将 +1 结果赋值给 i 。
  4. 同 <3> 一样

在没有任何特殊的处理的情况下,只有第一个操作(i = 0)是原子操作。

原子性定义:是指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行。

可见性

可见性是指:当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

总结

接下来我们看看下面的这段代码输入的值是多少?这也是面试题中经常回碰到的一道题。相信根据上面cpu执行volatile的解释应该可以知道下面的两个线程在读取到的i会保持一致,但是i++并非是原则操作。根据下图,如果i同时读取,那么都会读取到1,最终的执行结果可想而知,因此volatile无法保障复合操作的原则性。

public class Test{

    volatile int i = 0;

    public static void main(){
new Test().start();
} public void start(){ new Thread(){
public void run(){
i++;
System.out.println("i="+i);
}
}.start(); new Thread(){
public void run(){
i++;
System.out.println("i="+i);
}
}.start(); }
}

​有任何疑问,欢迎评论,转发给身边的朋友也带来快乐吧。

volatile最经典的应用:单例模式中的双重锁模式,想要了解单例模式的懒汉模式、饿汉模式、双重锁模式、静态内部内模式更深层的原理,可以关注下方的公众号回复"单例模式"即可获取文章。

笔者的微信公众号,每天一篇好文章:

并发编程-CPU执行volatile原理探讨-可见性与原子性的深入理解的更多相关文章

  1. Java并发编程知识点总结Volatile、Synchronized、Lock实现原理

    Volatile关键字及其实现原理 在多线程并发编程中,Volatile可以理解为轻量级的Synchronized,用volatile关键字声明的变量,叫做共享变量,其保证了变量的“可见性”以及“有序 ...

  2. Java并发编程之三:volatile关键字解析 转载

    目录: <Java并发编程之三:volatile关键字解析 转载> <Synchronized之一:基本使用>   volatile这个关键字可能很多朋友都听说过,或许也都用过 ...

  3. Java并发编程之验证volatile不能保证原子性

    Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...

  4. Java并发编程里的volatile。Java内存模型核CPU内存架构的对应关系

    CPU内存架构:https://www.jianshu.com/p/3d1eb589b48e Java内存模型:https://www.jianshu.com/p/27a9003c33f4 多线程下的 ...

  5. Java并发编程学习:volatile关键字解析

    转载:https://www.cnblogs.com/dolphin0520/p/3920373.html 写的非常棒,好东西要分享一下 Java并发编程:volatile关键字解析 volatile ...

  6. Java并发编程基础之volatile

    首先简单介绍一下volatile的应用,volatile作为Java多线程中轻量级的同步措施,保证了多线程环境中“共享变量”的可见性.这里的可见性简单而言可以理解为当一个线程修改了一个共享变量的时候, ...

  7. Java并发编程笔记之ConcurrentHashMap原理探究

    在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap. HashTable是一个线程安全的类 ...

  8. 干货:Java并发编程系列之volatile(二)

    接上一篇<Java并发编程系列之synchronized(一)>,这是第二篇,说的是关于并发编程的volatile元素. Java语言规范第三版中对volatile的定义如下:Java编程 ...

  9. Java 并发机制底层实现 —— volatile 原理、synchronize 锁优化机制

    本书部分摘自<Java 并发编程的艺术> 概述 相信大家都很熟悉如何使用 Java 编写处理并发的代码,也知道 Java 代码在编译后变成 Class 字节码,字节码被类加载器加载到 JV ...

随机推荐

  1. SD.Team颜色代码大全

    EEEEEE FFCCFF FF66FF FF00FF DDDDDD FFCCCC FF66CC FF00CC CCCCCC FFCC99 FF6699 FF0099 BBBBBB FFCC66 FF ...

  2. 自己的win7第一次使用RabbitMQ

    使用的过程中参考了https://www.cnblogs.com/longlongogo/p/6489574.html所写的内容 一.环境搭建 1.由于RabbitMQ使用Erlang语言编写,所以先 ...

  3. 使用 Typora 编辑器运用 Markdown 的语法编写文档

    Markdown 介绍 Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档. Markdown 语言在 2004 由约翰·格鲁伯(英语:John Gruber)创建. ...

  4. 循序渐进VUE+Element 前端应用开发(6)--- 常规Element 界面组件的使用

    在我们开发BS页面的时候,往往需要了解常规界面组件的使用,小到最普通的单文本输入框.多文本框.下拉列表,以及按钮.图片展示.弹出对话框.表单处理.条码二维码等等,本篇随笔基于普通表格业务的展示录入的场 ...

  5. Rocket - debug - TLDebugModuleInner - innerCtrl

    https://mp.weixin.qq.com/s/7UY99gEJ8QpVBJIohdqKhA 简单介绍TLDebugModuleInner中innerCtrl相关的寄存器. 1. innerCt ...

  6. ArcCore重构-打通Can各层ID配置

    https://mp.weixin.qq.com/s/JX7VZwyMqk_9iVMm_N2pxA https://mp.weixin.qq.com/s/5Y8Dt9j1-NQmnjfYhE19dg ...

  7. Rocket - decode - 几个问题

    https://mp.weixin.qq.com/s/pMsK_E4mQrm3QXdnp7nDPQ   讨论指令解码部分遗留的几个问题.     1. 最小项与蕴含项之间的关系   参考链接: htt ...

  8. JAVASE(九)面向对象特性之 : 继承性、方法重写、关键字super、

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 1.继承性 1.1 为什么要类的继承性?(继承性的好处) ①减少了代码的冗余,提高了代码的复用性:②更好 ...

  9. Spring ( 三 ) Spring的Bean的装配与生命周期、专用测试

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.对象的生命周期 1.IOC之Bean的生命周期 创建带有生命周期方法的bean public cla ...

  10. Java实现 LeetCode 1111 有效括号的嵌套深度(阅读理解题,位运算)

    1111. 有效括号的嵌套深度 有效括号字符串 定义:对于每个左括号,都能找到与之对应的右括号,反之亦然.详情参见题末「有效括号字符串」部分. 嵌套深度 depth 定义:即有效括号字符串嵌套的层数, ...