众所周知,无限制下多线程操作共享变量是危险的,为了保证线程安全语义,一般的建议是在操作共享变量时加锁,比方说在用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. Git从入门到熟练

    Git的特性 1. 分布式版本控制 集中式VS分布式 保存更新时的文件快照而非差异 (快照 :是文件系统中的概念或者技术:来自照相领域的概念,是指特定时间点的一个状态) 其他系统在每个版本中记录着各个 ...

  2. 剑指Offer - 九度1390 - 矩形覆盖

    剑指Offer - 九度1390 - 矩形覆盖2014-02-05 23:27 题目描述: 我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形.请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形 ...

  3. (转\整)UE4游戏优化 多人大地型游戏的优化(四)内存的优化

    施主分享随缘,评论随心,@author:白袍小道,当苦无妨 小道暗语: 1.因为小道这里博客目录没自己整,暂时就用随笔目录结构,所以二级目录那啥就忽略了.标题格式大致都是(原or转) 二级目录 (标题 ...

  4. ubantu 系统常见问题

    1 搜狗输入法错误: 删除home路径下的 .config/SouGouP...整个文件夹并重启 2  ubantu系统更新:sudo apt-get update  获取最近更新的软件包列表     ...

  5. 电信学院第一届新生程序设计竞赛题解及std

    首先非常感谢各位同学的参加,还有出题验题同学的辛勤付出 昨天想偷懒就是不想再把我C++11的style改没了,大家看不懂的可以百度一下哦,懒得再写gcc了,毕竟代码是通的 //代表的是行注释,所以那个 ...

  6. Struts2+DAO层实现实例02——搭建DAO基本框架并与Struts2组合

    实例内容 创建DAO(Data Access Oject)接口:BaseDAO 创建其实例化类:UserDAO 用于获取数据库struts中的userinfo表中的内容 创建User的Java Bea ...

  7. 201621123033 《Java程序设计》第3周学习总结

    第三周作业 1. 本周学习总结 初学面向对象,会学习到很多碎片化的概念与知识.尝试学会使用思维导图将这些碎片化的概念.知识点组织起来.请使用工具画出本周学习到的知识点及知识点之间的联系.步骤如下: 1 ...

  8. centos7安装Logwatch配合msmtp邮件客户端发送服务器监控分析日志

    ########################### #DATE 2016-07-29                         # #Authur by Denilas Yeung     ...

  9. 【bzoj2097】[Usaco2010 Dec]Exercise 奶牛健美操 二分+贪心

    题目描述 Farmer John为了保持奶牛们的健康,让可怜的奶牛们不停在牧场之间 的小路上奔跑.这些奶牛的路径集合可以被表示成一个点集和一些连接 两个顶点的双向路,使得每对点之间恰好有一条简单路径. ...

  10. Codeforces Round #428 (Div. 2) B

    B. Game of the Rows(贪心) 题意: 有k种颜色,每种有\(a_i\)个,将这k种颜色放在一个\(n * 8格子里\) 放置规则不能出现两个不同颜色在相邻的格子里,相邻的定义为在同一 ...