众所周知,无限制下多线程操作共享变量是危险的,为了保证线程安全语义,一般的建议是在操作共享变量时加锁,比方说在用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. Oracle exp,imp,expdp,impdp数据导入导出

    一.导出模式(三种模式)及命令格式 1. 全库模式 exp 用户名/密码@网络服务名 full=y file=路径\文件名.dmp log=路径\文件名.log 2. 用户模式(一般情况下采用此模式) ...

  2. linux下解压命令大全[转]

    本文是复制大神的博文, 供自己参考. 原文出处:http://www.cnblogs.com/eoiioe/archive/2008/09/20/1294681.html .tar 解包:tar xv ...

  3. USACO Section1.3 Barn Repair 解题报告

    barn1解题报告 —— icedream61 博客园(转载请注明出处)---------------------------------------------------------------- ...

  4. 【Remove Duplicates from Sorted List 】cpp

    题目: 第一次刷的时候漏掉了这道题. Given a sorted linked list, delete all duplicates such that each element appear o ...

  5. IOS与Unity交互

    学习了宣雨松的两篇Unity和IOS交互的文章,自己动手做了下,遇到了些问题,在此记录 先说IOS发送消息给Unity:(文章地址:http://www.xuanyusong.com/archives ...

  6. SPOJ 364 Pocket Money 简单DP

    跟矩阵链乘同类型的题…… 输出用%llu不是%I64u…… 几组数据: 141+2*4+3*4+5*00*5*6+7*3+23+0+6+7+0+44*5+7*1*1+12*0+3*4*0+5*6+7+ ...

  7. 通过nbviwer在线分享python notebook

    在数据科学计算中,jupyter-notebook是一个很得力的助手,但是Notebook写完之后如何与他人分享呢?我们可以使用nbviwer. 具体思路: 具体的方法如下: 本地编写ipython ...

  8. Comparable和Comparator的学习笔记

    目录 Comparable和Comparator的实现 Comparable接口 Comparator接口 总结 参考自 今天在项目开发中,遇到要对List中的对象按照对象某一属性进行排序的问题,我发 ...

  9. 【Luogu】P3288方伯伯运椰子(消圈定理)

    题目链接 分数规划题,详见luogu题解 #include<cstdio> #include<cstring> #include<cctype> #include& ...

  10. JavaScript—获取本地时间以12小时制显示

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...