java多线程之三——volatile

在多线程编程中,Synchronized 和 volatile 都扮演者重要的角色,前面的文章我们已经了解了java内置锁Synchronized ,它保证了并发过程中的可见性与原子性,避免了共享数据的错误。
而 Volatile可以看做是轻量级的 Synchronized,它只保证了共享变量的可见性。在线程 A 修改了被 volatile 修饰的共享变量之后,线程 B 能够读取到正确的值。在 关于JMM 的文章中我们了解到 java 在多线程中操作共享变量的过程中,会存在指令重排序与共享变量工作内存缓存的问题。

volatile作为一个修饰符,使用很简单,但是它背后做了多少工作呢?

首先我们需要明白,本地内存是一个抽象概念,包括缓存、读写缓冲区、寄存器,甚至编译器重排序和cpu重排序。JVM按照JMM规范对volatile进行特殊处理,从而实现在CPU对该变量的特殊处理。

volatile底层原理

计算机系统中,硬盘负责存储数据, 但是数据交换速度慢,CPU 运行速度非常快,CPU直接硬盘的数据交换效率非常低,于是产生了内存,通过内存与 CPU 进行数据交换,但是内存的速度依旧不够快,严重拖慢整体的运行效率,故而在 CPU 内部添加了高速缓存,作为 CPU的临时存储器,与内存的数据交互。

  • 在单核CPU中,多线程都在一个CPU中进行运行,共用一份缓存,对同一个共享变量的使用,而不会出现数据可见性的问题
  • 而多核CPU由于多线程可能分配在不同的CPU,这种情况下进行计算时,就会出现一个CPU内核计算完成,并没有同步回主内存,而其他CPU无法使用最新的数据,而出现了可见性问题。

通过添加volatile修饰,通过JVM的优化,最后反应到CPU上,先从内存获取数据,存储在高速缓存中,然后再从高速缓存中获取数据进行计算,计算完成后的值并不会立即刷新回主内存中,而其他 CPU 这时并不知道变量值已经改变,使用的还是之前的变量值进行计算,这就产生了数据错误。这种机制类似我们之前讲过的 JMM 中主内存于工作内存的关系。

我们知道,javac 编译器将 .java 代码编译成为 .class 字节码,JVM 通过解释器与即时编译器(JIT)运行字节码中的指令,将字节码指令翻译称为具体的机器码指令,而被 volatile 修饰的共享变量,在翻译成为机器码的过程中为其赋值操作添加特殊机器码指令前缀Lock xxxx

public class Test{
  private volatile  int i=1;//被 volatile 修饰

  //线程A修改
  public void setVar(){
        i=2;
  }

 //线程B获取
  public int getVar(){
       return i;
  }
}

在执行此条指令时,Lock 指令有两个作用:

  • 使本CPU的缓存写入内存
  • 上面的写入动作也会引起别的CPU或别的内核中的缓存无效,

所以通过这样一个指令前缀,可以让对volatile变量的修改对其他CPU可见。

指令重排序

还是上文中的Lock前缀的作用,为什么它能禁止指令重排序呢?

从JMM角度讲:

在JMM的逻辑实现中,当操作一个变量 执行putfield指令(为变量赋值) 时,JVM会检查此变量是否是被volatile修饰的,如果是的话,JVM会为该变量添加内存屏障,用于隔离该变量与前后操作,从而禁止volatile变量的操作与前后操作的乱序。

摘自java并发编程的艺术:
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来
禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总
数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
·在每个volatile写操作的前面插入一个StoreStore屏障。
·在每个volatile写操作的后面插入一个StoreLoad屏障。
·在每个volatile读操作的后面插入一个LoadLoad屏障。
·在每个volatile读操作的后面插入一个LoadStore屏障。


image.png

从CPU执行角度讲:

以上的内存屏障就会在执行时生成相应带有Lock前缀的机器码(全面已提及)。在CPU中,程序的执行计算是由CPU在不影响逻辑结果的前提下分配给不同的电路去处理逻辑,Lock指令前缀刷新回内存,必然是在此指令之前的运算全部计算完成之后,取得正确的结果才会刷新回内存的,所以这也形成了一道内存屏障,表示对该变量操作之前的操作不会乱序到其后,其后的操作不会乱序到之前。

综上述,volatile的实现就是一个Lock指令前缀的作用。

使用注意事项

volatile虽然保证了可见性,但是它不保证原子性。

诸如i++之类的语句,在执行时的步骤:

  1. 从内存取值,放到CPU缓存中
  2. CPU中i+1
  3. 存在缓存中
  4. 刷新会内存

可见这这并不是单纯的赋值操作,而是有在第4步完成之前,其他CPU内核是看不到数值变化的,而如果仅用volatile修饰的话,仅仅保证了第3部完成之后,会立即刷新回内存,但不会保证第2步计算与第3,4步的原子性。如果线程A计算+1之后,没有刷回内存,线程B也+1,那么最后的结果肯定是比期望的结果小的。所以在多线程操作++时,还是应该使用synchronized等同步操作保证原子性。

volatile比synchronized轻量,只保证可见性。正因如此,在java.util.concurrent中AQS使用了被volatile修饰的变量来标记状态,实现了灵活多样的各种锁,补充了内置锁synchronized的互斥等缺点。

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

推荐阅读更多精彩内容