众所周知,无限制下多线程操作共享变量是危险的,为了保证线程安全语义,一般的建议是在操作共享变量时加锁,比方说在用synchronized关键字修饰的方法内读写共享变量。

但是synchronized开销较大,有没有更轻量更优雅的解决方案呢?

volatile是轻量级的synchronized,在正确使用的前提下,它可以达到与synchronized一样的线程安全的语义,而且不会带来线程切换的开销。

volatile的作用是什么?

volatile保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它在某些情况下比synchronized的开销更小。

这句话可能难以理解,我来举个例子。

在我之前写的这篇文章<Ticket Lock, CLH Lock, MCS Lock>中,第一个给出的naive lock的例子里,flag变量被声明为volatile。

如果flag不是volatile而是普通变量,那会发生什么呢?

想象一个场景:线程A正在占有锁,线程B自旋监听flag变量。现在线程A退出临界区,将flag置为false。但是线程B能立即观察到flag的变化吗?

很不幸,不一定。

因为现代CPU中有多个core,每个core都有各自的高速缓存(cache),线程A对flag的修改只写在了cache上,需要一段不确定的时间才会被刷新到主存中。而线程B所在的core也会将flag变量缓存在cache中,这样就算主存中的flag变量发生了变化,线程B还是不一定能看到。

所以,如果flag是普通变量,naive lock是不成立的。

而如果将flag设置为volatile类型,JVM就会保证任何对flag变量的写操作会被立即刷新到主存里,同时还会让其他缓存了flag变量的core的对应的cache line失效,强迫其他线程必须从主存中读取最新的值。

这样就实现了共享变量被一个线程修改,其他线程立刻就能读到的语义。

那么volatile关键字的底层原理是什么呢?它是如何让写操作直接被刷新到主存,又是如何让其他core的cache line失效的呢?

如果观察编译出来的机器码,会发现在对volatile变量的写操作之后,会附加一条指令

lock addl $0x0,(%esp);

很容易可以看出,addl $0x0,(%esp) 这句话本身是没有任何作用的,效果与nop这样的空转指令等同,但是前面的lock前缀,有点意思。

查阅Intel的<Intel® 64 and IA-32 Architectures Software Developer’s Manual>,里面有这样一段话

8.1.4 Effects of a LOCK Operation on Internal Processor Caches

For the Intel486 and Pentium processors, the LOCK# signal is always asserted on the bus during a LOCK operation, even if the area of memory being locked is cached in the processor.

For the P6 and more recent processor families, if the area of memory being locked during a LOCK operation is cached in the processor that is performing the LOCK operation as write-back memory and is completely contained in a cache line, the processor may not assert the LOCK# signal on the bus. Instead, it will modify the memory location internally and allow it’s cache coherency mechanism to ensure that the operation is carried out atomically. This operation is called “cache locking.” The cache coherency mechanism automatically prevents two or more processors that have cached the same area of memory from simultaneously modifying data in that area.

大概意思是说,遇到lock指令,对应的core上的cache line会被强制刷回到主存中。然后由于缓存一致性协议的效果(参见我的这篇博客<缓存一致性协议>),其他core上的对应的cache line也会被强制设置为invalid。

于是volatile的语义就实现了,仅需这一条lock指令。

但是volatile能保证原子性吗?比方说如果我们多线程对一个volatile型的变量做自增操作,这是线程安全的吗?
答曰:并不是。

我们想象有两个线程对volatile类型的变量count做自增操作,count初始值为0,两个线程同时拿到了0,同时自增为1,然后同时写回,这样count的结果还是1,不符合期望。

那么我们应该怎么办呢?

参考AtomInteger,使用UNSAFE提供的CAS操作更新count,在上面的场景中,两个线程同时执行CAS(count, 0, 1),只有其中一个线程能执行成功,另外一个线程失败重试,读取新的count值,然后执行CAS(count, 1, 2),这一次执行成功了,那么count的最终值是2。符合期望。

参考资料:

聊聊并发(一)深入分析Volatile的实现原理

深入理解volatile关键字

Intel® 64 and IA-32 Architectures Software Developer’s Manual,8.1.4节,p257

解析Java的volatile关键字的更多相关文章

  1. 解析java中volatile关键字

    在Java多线程编程中经常volatile,有时候这个关键字和synchronized 或者lock经常有人混淆,具体解析如下:  在多线程的环境中会存在成员变量可见性问题: java的每个线程都存在 ...

  2. 深入解析Java中volatile关键字的作用

    转(http://m.jb51.net/article/41185.htm)Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块 和 volatile 关键字机制 在java线 ...

  3. 【转】java中volatile关键字的含义

    java中volatile关键字的含义   在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  4. 转:java中volatile关键字的含义

    转:java中volatile关键字的含义 在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  5. Java中Volatile关键字详解 (转自郑州的文武)

    java中volatile关键字的含义:http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html 一.基本概念 先补充一下概念:J ...

  6. Java中volatile关键字解析

    一.内存模型的相关概念 大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入.由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存 ...

  7. java复习volatile关键字解析

    转自https://www.cnblogs.com/dolphin0520/p/3920373.html volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受 ...

  8. 关于java的volatile关键字与线程栈的内容以及单例的DCL

    用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新的值.volatile很容易被误用,用来进行原子性操作. package com.guangshan.test; pub ...

  9. Java 并发 —— volatile 关键字

    volatile 修饰变量等于向编译器传达如下两层含义: 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的. 禁止进行指令重排序. volat ...

随机推荐

  1. 《Cracking the Coding Interview》——第18章:难题——题目5

    2014-04-29 01:51 题目:你有一个文本文件,每行一个单词.给定两个单词,请找出这两个单词在文件中出现的其中一对位置,使得这两个位置的距离最短. 解法:我的思路是建立倒排索引,计算出所有单 ...

  2. ADB命令总结(1)

    今日继续学习ADB,使用真机来操作,因此把所用到的命令总结如下: 一,模拟按HOME键 adb shell input keyevent KEYCODE_HOME 二,滑动手机屏幕 从(x1,y1)滑 ...

  3. 4.实现简单的shell sed替换功能

    # -*- coding:utf-8 -*- # Author: JACK ZHAO # 程序1: 实现简单的shell sed替换功能 import sys #判断参数个数 if len(sys.a ...

  4. Python学习5,三级菜单实例

    _author_ = "Happyboy" data = { '北京':{ "昌平":{ "沙河":["Happyboy" ...

  5. java5初学

    1.Eclipse快捷键 alt + / 代码提示,例如自动创建main方法ctrl + d 删除当前行ctrl + alt + up/down 复制当前行alt + up/down 交换行ctrl ...

  6. python学习总结---函数使用 and 生成器

    # 函数使用 ### 生成器 - 使用场景 在使用列表时,很多时候我们不会一下子使用全部数据,通常都是一个一个使用,但是当数据量比较大的时候,定义一个大的列表将会是内容使用突然增大.为了解决此类问题, ...

  7. 第二阶段团队冲刺-two

    昨天: 处理第一次启动服务器500的问题. 今天: 修复博客作业查询功能. 遇到的问题: 无.

  8. GCD 开发详情

    目录 一.简介 二.dispatch Queue - 队列 三.dispatch Groups - 组 四.dispatch Semaphores - 信号量 五.dispatch Barriers ...

  9. hdu 6102 GCDispower

    多校6 GCDispower(容斥) 题意: 给一个长度为\(n\)的排列 给q组询问 每次查询\(L,R\)内的答案 \(\sum_{i=L}^{R}\sum_{j=i+1}^{R}\sum_{k= ...

  10. vue数组对象修改触发视图更新

    直接修改数组元素是无法触发视图更新的,如 this.array[0] = { name: 'meng', age: 22 } 修改array的length也无法触发视图更新,如 this.array. ...