volatile详解

一、volatile的作用详解

1、防重排序

        在并发环境下的单例实现方式,我们通常可以采用双重检查加锁(DCL)的方式来实现需要在变量singleton之间加上volatile关键字。因为操作系统可以对指令进行重排序,在多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。

2、实现可见性

       可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区—线程工作内存使用volatile关键字会把数据副本更新到主存中,另外一个线程也可获取到最新的数据。

3、保证原子性:单次读/写

        基于volatile保证单次的读/写操作具有原子性的理解

问题1: i++为什么不能保证原子性?

       对于原子性,需要强调一点,也是大家容易误解的一点:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量。但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。

i++其实是一个复合操作,包括三步骤:

1)读取i的值。

2)对i加1。

3)将i的值写回内存。

        volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。

问题2: 共享的long和double变量的为什么要用volatile?

        因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。

      目前各种平台下的商用虚拟机都选择把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不把long 和 double 变量专门声明为 volatile,多数情况下也是不会错的。


二、volatile 的实现原理

(一)volatile 可见性实现

        volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现:

       内存屏障,又称内存栅栏,是一个 CPU 指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止+ 特定类型的编译器重排序和处理器重排序。插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。如果对声明了 volatile 的变量进行写操作JVM 就会向处理器发送一条 lock 前缀的指令将这个变量所在缓存行的数据写回到系统内存。

       为了保证各个处理器的缓存是一致的,实现了缓存一致性协议(MESI)。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,然后重新从系统内存中把数据读到处理器缓存里。所有多核处理器下还会完成:当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。volatile 变量通过这样的机制就使得每个线程都能获得该变量的最新值。

1)lock 指令

       在 Pentium 和早期的 IA-32 处理器中lock 前缀会使处理器执行当前指令时产生一个 LOCK# 信号会对总线进行锁定其它 CPU 对内存的读写请求都会被阻塞,直到锁释放。后来的处理器,加锁操作是由高速缓存锁代替总线锁来处理。因为锁总线的开销比较大这种场景多缓存的数据一致通过缓存一致性协议(MESI)来保证

2)缓存一致性

       缓存是分段(line)的,一个段对应一块存储空间,称之为缓存行,它是 CPU 缓存中可分配的最小存储单元,大小 32 字节、64 字节、128 字节不等,这与 CPU 架构有关,通常来说是 64 字节

       LOCK# 因为锁总线效率太低,因此使用了多组缓存为了使其行为看起来如同一组缓存那样。因而设计了缓存一致性协议缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于 " 嗅探(snooping)" 协议

       所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个 CPU 缓存可以读写内存)

        CPU 缓存不仅仅在做内存传输的时候才与总线打交道而且还不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。 当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器写内存,其它处理器马上知道这块内存在它们的缓存段中已经失效。

(二)volatile 有序性实现

volatile 的 happens-before 关系

       happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读

volatile 禁止重排序

        为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序。而volatile就是屏蔽指令的代码实现。

volatile 重排序规则表

       " NO " 表示禁止重排序。为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM 采取了保守的策略。

在每个 volatile 写操作的前面插入一个 StoreStore 屏障。

在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。

在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。

在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

       volatile 写是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。

内存屏障说明

StoreStore 屏障 禁止上面的普通写和下面的 volatile 写重排序。

StoreLoad 屏障 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。

LoadLoad 屏障 禁止下面所有的普通读操作和上面的 volatile 读重排序。

LoadStore 屏障 禁止下面所有的普通写操作和上面的 volatile 读重排序。



三、volatile 的应用场景

使用 volatile 必须具备的条件:

1、对变量的写操作不依赖于当前值。

2、该变量没有包含在具有其他变量的不变式中。

3、只有在状态真正独立于程序内其他内容时才能使用 volatile。

模式1:状态标志

        实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志。用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

模式2:一次性安全发布(one-time safe publication)

        缺乏同步会导致无法实现可见性

模式3:独立观察(independent observation)

       安全使用 volatile 的另一种简单模式是定期 发布 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

模式4:volatile bean 模式

       在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。

模式5:开销较低的读-写锁策略

       volatile 的功能还不足以实现计数器如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

模式6:双重检查(double-checked)

       就是我们上文举的例子。单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。

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