【Java并发(二)】--volatile详解


如未作特殊说明,文章均为原创,转发请注明出处。

[TOC]

前提: 在Windows系统下如何编译hsdis-amd64.dll和hsdis-i386.dll

[如何在windows平台下使用hsdis与jitwatch查看JIT后的汇编码

使用JITWatch查看JVM的JIT编译代码

简介

​ Volatile可以简单的理解为Java提供的一个比Synchronized更加轻量级的同步机制。但是大多数程序员都不能正确、完整的理解和使用它。一般在遇到多线程处理数据竞争时,一律使用Synchronized来进行同步。

​ 但是我们都知道Synchronized是一个重量级的锁,虽然jvm对其进行了大量的优化,但是在volatile相较于synchronized来说是一个轻量级的锁。如果用它来修饰一个变量,那么会比使用synchronized成本更加的底,因为volatile不会引起线程的上下文切换和调度。并且Java语言规范对volatile有如下定义:

Java语言是允许线程访问共享变量的,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独地获取该变量。Java语言提供了Volatile,在某些情况下比锁妖更加地方便。如果一个字段被声明成volatileJava线程内存模型确保所有线程都可以看到该变量地值,并且保持一致性。

​ 其实就是Java就是提供了volatile来修饰变量,来确保该变量在线程中是可见的。保证了其原子性。(那么在什么时候该使用volatile呢?有这么简便轻便的锁为什么还存在并且使用synchronized这种相对而言非常笨重的锁呢?)


那么在了解volatile实现原理之前,首先要了解其实现原理相关地CPU术语与说明。

术语 英文单词 术语描述
内存屏障 memory barriers 是一组处理器指令,用于实现对内存操作地顺序限制
缓冲行 cache line 缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期
原子操作 atomic operations 不可中断的一个或一系列操作
缓存行填充 cache line fill 当处理器识别到从内存中读取操作时可缓存的,处理器读取整个缓存到适当的缓存(L1、L2、L3的或所有)
缓存命中 cache hit 如果进行告诉缓存行填充操作的内存仍然时下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存中读取
写命中 write hit 当处理器及那个操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存中,而不是写回到内存,这个操作被称为写命中
写缺失 write misses th cache 一个有效的缓存行被写入到不存在的内存区域

那么从上面的CPU术语更加深入的理解CPU是怎样操作数据的读写的

​ 计算机在运行程序时,每条指令都是在CPU中完成的,在执行这些操作时,肯定需要跟内存打交道,考虑到效率的问题,CPU提出了缓存行的概念。因为读写主存中的数据肯定没有在CPU中执行速度快。那么如果所有的交互都是在跟主内存打交道,那么速率可想而知。为了高效的性能,CPU提出了缓存行填充的概念,当处理器识别到从内存中读取操作课缓存时,处理器会将读取的整个缓存到适合的缓存中。这样就大大的提高了效率,这是这种高速缓存是以CPU为单位的,并不能与其他CPU共享(当今应该没有单核的电脑了吧?😁)。CPU高速缓存为某个CPU独有的,只与该CPU运行的线程有关。

​ 有了CPU高速缓存,它解决了读取的效率问题,可是带来了另外一个问题,就是数据的 一致性 的问题。当一个CPU将当前变量缓存到缓存行后,其中任何操作都不会跟主内存打交道,直到操作结束后,才会将最终的数据刷新到主内存中,那么在这途中,如果有其他的线程另外的CPU来读取并且缓存该数据,就会出现数据不一致的问题。

举例

1541039220349.png

int i = 0;
i++;

​ 当第一个线程进入程序,并且读取i的值并且缓存到缓存行进行1次叠加操作,(理想值为1);

​ 但是就在第一个线程还没操作完,第二个线程也进入程序操作该变量,那么由线程一修改的值并不会即使的刷新给线程二,所以在线程二操作完之后,i 的值还是为1.出现了大BUG了!!!!

此时java针对此问题提出了两种解决方法

  1. 通过总线锁的方式,实现数据的一致性。
  2. 通过缓存一致性协议

下面会详细分析两种方式的实现方式与利弊。

​ 总线锁:字面理解,就是在总线程上加上一把锁,次方式采用的一种独占的方式来实现的,即只能一个CPU能够运行,其他CPU都得阻塞,效率较为低下。

​ 缓存一致性协议(MESI协议):它确保了每个缓存中使用的共享变量的副本是一致的。在多核处理器系统中进行操作时,处理器会使用嗅探技术保证它的内部缓存、系统内存和其他处理器缓存的数据在总线上保持一致,当嗅探技术,方法总线上的数据,与缓存中的数据不一致时,那么正在嗅探的处理器会将它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。


CPU使用嗅探技术保证数据的一致性(遵循缓存一致性机制)

那么针对数据的一致性,则需要明白JVM的内存模型的三大特征

  • 原子性
  • 可见性
  • 有序性

原子性 (Atomicity):由Java内存模型来直接保证的原子性操作的read、load、assign、use、store和write。所谓的原子性就是再一个操作或者一系列操作中,要不全部完成,要么全部失败。(Java内存模型中提到我们大致可以认为基本数据类型的访问读写是具备原子性的。【其中例外long和double的非原子性协定。但这种情况几乎不会发生。】)

可见性 (Visibility):可见性是指当一个线程在修改一个变量之后,其他的线程能够立即得到这个修改后的新值。在这里普通变量并不能实现这种所谓的可见性。而被volatile修饰过的变量则可是实现这种可见性。在变量修改时会将新的值第一时间同步到主内存中,那么其他有缓存该变量的线程通过嗅探和缓存一致性协定发现该线程缓存的值跟主内存的值并不一致时会将当前线程的缓存的值无效。在下次调用该变量时会访问主内存,实现线程之间的可见性。

​ 除了volatile之外,Java还有两个关键字可以实现可见性,即synchronizedfinal。同步块的可见性是由“对一个变量施行unlock解锁之前,必须将修改完的变量写会到主内存中”。而final修饰的变量的可见性是指,一但变量被指定为final那么该变量只要没有将this的引用传递出去(这里涉及到的是this引用逃逸),那么在其他线程中就能看见该final字段的值。如下列代码所示:i与j都具备可见性。无须同步就能被其他线程正确访问。

public static final int i;

public final int j;

static {
    i = 0;
    // do something
}

{
    // 也可以选择在构造函数中初始化
    j = 0;
    // do something
}
    

有序性 (Oredering)Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另外一个线程,那么所有的操作都是无序的。

​ 前半句:指的是线程内表现为串行语义(within-Thread As-If-Serial Semantics)。

​ 后半句:“指令重排序”现象和“工作内存与主内存同步延迟”现象

其中volatile关键字本身就包含了禁止指令重排序的语义。

那么valatile是怎么保证可见性的呢?所以我们需要运用工具来查看JIT编译器生成的汇编指令查看volatile进行写操作时,CPU会做什么事情。

​ 通过了解到CPU时如何实现缓存一致性后,我们就很容易的知道volatile的作用与原理了。

通过以下代码,在使用JIT编译工具来查看volatile的真正面目。(文章顶端有关于如何查看JIT汇编指令的教程。)

-XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssembly
-XX:+LogCompilation
-XX:LogFile=jit.log

我使用的JIT编译代码


public class VolatileBarrierExample {
    long a;
    volatile long v1 = 1;
    volatile long v2 = 1;

    void readAndWrite() {
        long j = v1;
        long i = v2;
        a = i + j;
        v1 = i + 1;
        long v = v1;
        v2 = j * 2;
    }

    public static void main(String[] args) {
        final VolatileBarrierExample ex = new VolatileBarrierExample();
        for (int i = 0; i < 50000; i++) {
            ex.readAndWrite();
        }
    }
}

我们需要注意上述程序中a = i + jv1 = i + 1,从程序的角度来看,当前 a的值应该与v1的值一样都等于2.但其实不然,因为程序中a 并没有使用volatile修饰,但是v1v2都有修饰,因为v1、v2、保证了其原子性。但是变量a并不能保证。

并且 利用JIT汇编语言可以清楚的看出,在使用volatile修饰后的变量,在进行任何操作时,都会多处一行以Lock前缀的指令。

long j = v1;

该代码对应的指令:

jit_01
a = i + j;
jit_02
v1 = i + 1;
jit_03

很明显的看出,在对使用volatile修饰的变量v1进行写操作时,会多出一个前缀为lock的指令(程序下面的v2 = j * 2也是如此)。该指令会在多核处理器下回引发两件事情。

  1. 将当前处理器缓存行的数据写会到系统内存中。
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效(CPU的嗅探,来保证数据的一致性)。

总结:

volatile看起来非常的简单,并且轻巧。但是也存在很多弊端。相较于synchronized来说,在某些场景可以代替synchronized,但又不能完全取代。因为在使用volatile时,大部分只能修饰关键变量,并且如果大量使用volatile反而会影响运行效率。因为volatile会禁止指令重排序,禁用CPU优化。在使用它必须满足如下两个条件

  1. 对变量的写操作不依赖当前值;
  2. 该变量没有包含在具有其他变量的不变式中;

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


参考资料

  1. 方腾飞:《Java并发编程的艺术》
  2. 周志明:《深入理解Java虚拟机》

该文章仅供作者本人学习之用,有不足之处还望指正!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容