Java 多线程 : volatile

在多线程并发编程中,锁的运用很常见。synchronized 的几种运用方式,相信大部分 Java 程序员已经很熟悉。而 volatile 作为轻量级的 synchronized,不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。

在现代计算机系统中,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓存的内存读写了。

下面是计算机系统中处理器、高速缓存、主内存间的交互关系:

计算机系统内存模型

基于高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性。

下面是Java中线程、主内存、工作内存交互关系:

Java 内存模型

Volatile 的官方定义

Java 语言规范第三版中对 volatile 的定义如下: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了 volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

内存不可见的含义

在 JVM 中,对于多线程应用,如果多个线程同时使用某个没有 volatile 修饰的变量时,每个线程会从主内存拷贝目标变量到当前线程的工作内存中,然后在各自的工作内存进行具体的操作。

可见性的定义:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得到这个修改。

在上面的情景中,不同线程的对主内存变量副本的操作不能够即时的反馈到主内存区,其他线程的工作内存更是无法感知,内存不可见。

如何保证内存可见

volatile 如何实现内存可见的呢?
在x86处理器下通过工具获取JIT编译器生成的汇编指令:

语言 代码片段
Java instance = new Singleton(); </br> //instance 是 volatile 修饰变量
汇编 0x01a3de1d: movb $0x0,0x1104800(%esi);</br>0x01a3de24: lock addl $0x0,(%esp);

有 volatile 变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。

  • 将当前处理器缓存行的数据回写到系统内存。
  • 这个回写内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

也就是说,处理器为了提高处理速度,不直接和内存通讯,而是先将内存数据拷贝到缓存后再操作(同上图)。如果变量声明了 volatile,那么处理器读取操作会直接和内存进行通讯,将变量所在缓存行的数据直接写入系统内存或者直接读取系统内存数据。但是如果其他处理器缓存的数据仍然是旧的数据,那么再执行计算操作就是无意义的。所以这里就存在缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检测自身缓存是否过期,如果检测到自己缓存行对应的数据被修改,那么会将当前处理器缓存行设置为无效状态�。当处理器需要该数据进行操作时,会强制从系统内存重新加载到当前处理器缓存中。

缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。

具体的专有名词及细节可以看文末的 reference(本节内容摘录自文末的参考文章).

保证对 64 位变量读写的原子性

JVM 可以保证对 32位 数据读写的原子性,但是对于 long 和 double 这样 64位 的数据的读写,会将其分为 高32位 和 低32位 分两次读写。所以对于 long 或 double 的读写并不是原子性的,这样在并发程序中共享 long 或 double 变量就可能会出现问题,于是 JVM 提供了 volatile 关键字来解决这个问题:

使用 volatile 修饰的 long 或 double 变量,JVM 可以保证对其读写的原子性。

但是,此处的 “写” 仅指对 64位 的变量进行直接赋值。

指令重新排序对 volatile 的影响

如果一个操作不是原子操作,那么 JVM 便可能会对该操作涉及的指令进行 重排序优化。重排序即在不改变程序语义的前提下,通过调整指令的执行顺序,尽可能达到提高运行效率的目的。

int a = 1;
int b = 2;

a++;
b++;

可能会被重新排序为:

int a = 1;
a++;

int b = 2;
b++;

这样看是没什么影响的。

但当一个变量是 volatile 修饰时,指令重排序就可能会出现问题。

public class Counter {
    private int numA;
    private int numB
    private volatile int numC;

    public void update(int numA, int numB, int numC){
        this.numA  = numA;
        this.numB = numB;
        this.numC   = numC;
    }
}

当 update 方法调用时,numA,numB,numC 的新值都会直接写入系统内存。但是如果重新排序成这样:

public void update(int numA, int numB, int numC){
    this.numC  = numC;
    this.numA   = numA;
    this.numB = numB;
}

修改 numC 变量时,A和B的值仍会写入主内存,但这一次是在A和B的新值写入之前发生的。因此,其他线程无法正确地看到A和B的新值。重新排序的指令的语义已经改变。

为了解决指令重新排序这个难题,Java volatile 关键字除了提供可见性保证之外,还提供“happens-before”保证:

  • 如果读取/写入其他变量的操作最初就发生在写入 volatile 修饰变量之前,那么指令重新排序时,不允许这个操作被排到被 volatile 修饰的变量写入之后;注意,对于其他变量的操作最初发生在写入 volatile 修饰变量之后的,那么重新排序是仍然有可能排到 volatile 修饰变量写入之前。

  • 如果读取/写入其他变量的操作最初就发生在写入 volatile 修饰变量之后,那么指令重新排序时,不允许这个操作被排到被 volatile 修饰的变量写入之前;注意,对于其他变量的操作最初发生在写入 volatile 修饰变量之前的,那么重新排序是仍然有可能排到 volatile 修饰变量写入之后。

上述的“happens-before”保证正在被实施。

必须保证操作原子性

对 volatile 修饰的变量操作时,即使每次都是从系统内存读取,都是直接写入系统内存,仍然会存在问题。

当多个线程同时写入一个 volatile 变量时,例如 i++ 操作。对于 i++ 这个语句,事实上涉及了 读取-修改-写入 三个操作:

  • 读取变量到栈中某个位置
  • 对栈中该位置的值进行自增
  • 将自增后的值写回到变量对应的存储位置

volatile 变量只能保证可见性,在不符合以下两条规则的运算场景中,仍需要通过加锁(使用 synchronized 或 java.util.concurrent 中的原子类)来保证原子性。

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

合适的使用场景

读取和写入一个 volatile 变量会直接和系统内存通信,对比与处理器缓存通信的消耗要大得多。访问 volatile 变量还防止指令重新排序,这是一种正常的性能增强技术。所以只有在真正需要变量强制可见性时才应该使用。

具体的几种场景可以参考正确使用 Volatile 变量

参考资料:

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