众所周知,无限制下多线程操作共享变量是危险的,为了保证线程安全语义,一般的建议是在操作共享变量时加锁,比方说在用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. iOS远程消息推送原理

    1. 什么是远程消息推送? APNs:Apple Push Notification server 苹果推送通知服务苹果的APNs允许设备和苹果的推送通知服务器保持连接,支持开发者推送消息给用户设备对 ...

  2. SpringMVC 集成 Freemarker 模板引擎

    本文通过 maven 项目中集成 1.引入 SpringMVC 与 Freemarker 需要的依赖 <!-- SpringMVC --> <dependency> <g ...

  3. python的inspect模块

    一.type and members 1. inspect.getmembers(object[, predicate]) 第二个参数通常可以根据需要调用如下16个方法: 返回值为object的所有成 ...

  4. WIN10把照片查看器设为默认看图软件

    WIN10默认是PHOTO,没有以前WIN7的照片查看器好用,要改回来的方法如下:   在注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Photo ...

  5. redis的socket event loop

    很早之前就因为nosql就听说了redis,直到去年才真正去了解,只能说相见恨晚. 因为数据库相关,我以为这应该是个庞然大物,万万没想到,源码不到2M,所以,我不知道该说啥了... 还是来点靠谱的: ...

  6. ConfigurationManager.ConnectionStrings 类库 取不到值 报错 初始化错误

    是因为我把 config 文件写在了 类库中,(擦,很久之前就处理过好多次,总是忘记 写个文章记录下来) 其实应该放在 主目录底下的 web.comfig 里 就是网站项目的 配置文件里,类库找的 是 ...

  7. GYM - 101147 C.The Wall

    题意: 长和宽分别为M+N/2,N的矩形中.有很多敌人的点.有两种方法消灭敌人. 1.N个桶,第i个桶可以消灭i-1<=x<i中的敌人.2.M个摆(半圆)每个摆可以消灭距离他前面不超过1以 ...

  8. GYM - 101147 B.Street

    题意: 大矩形代表市场,大矩形当中有很多小矩形样式的伞.这些小矩形都贴着大矩形的左边或者右边且互不相交.小矩形以外的地方都是阳光.求经过大矩形时在阳光下的最短时间. 题解: 最短路的做法.起点和终点与 ...

  9. 在Ignite中使用k-最近邻(k-NN)分类算法

    在本系列前面的文章中,简单介绍了一下Ignite的线性回归算法,下面会尝试另一个机器学习算法,即k-最近邻(k-NN)分类.该算法基于对象k个最近邻中最常见的类来对对象进行分类,可用于确定类成员的关系 ...

  10. 3.1 Java以及Lucene的安装与配置

    Lucene是Java开发的一套用于全文检索和搜索的开源程序库,它面向对象多层封装,提供了一个低耦合.与平台无关的.可进行二次开发的全文检索引擎架构,是这几年最受欢迎的信息检索程序库[1].对Luce ...