对volatile变量规则的独家理解

《Java并发编程的艺术》在第三章介绍了 JMM(Java 内存模型),在第 3.7.3 小节里提到了 6 条规则,即大名鼎鼎的 happens-before 规则。其中我认为入门玩家比较难理解的是第三条 —— volatile 变量规则。

volatile 变量规则

对于一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

咋一看好像说了很多,又好像什么都没说。中文社区内的解读大多是搬一些公式化的句子和一些模板化的代码,再深度解读一番 Store Load Barrier ,让你感觉他说得没错,但是不知道和这句话有什么关系,就如同没有这句话,程序那样运行依旧是天经地义的。

我甚至有点怀疑是不是因为翻译的原因导致这句话如此简单却难以理解。为此我特地找了一下原文,有兴趣的可直接看:

http://www.cs.umd.edu/~pugh/java/memoryModel/CommunityReview.pdf

原文在第三小节,如图 1:

图 1

原文就是这么直白而难懂。

渐近式理解

从一个刚接触 JMM 的萌新的角度来看,也许会产生以下想法,一一解读:

  1. 根据规则,“写先于读”,那是不是 volatile 变量不写就不能读呢?

    • 否。这个“不写”的前提是不存在的,哪怕是不主动赋值,JVM 也会在初始化时为其赋个默认值。
  2. 单线程下,先改写 volatile 的值,再读,毫无疑问能读到改后的值。

    • 是。顺序原则。
  3. 多线程下呢?如果是根据“写先于读”的话,读会等待写吗?也就是说如果发现同时有多个线程,其中在修改一个 volatile 值,其它都是读的线程,是不是读线程会阻塞,等写线程完成后才去读?

    • 否。写个简单的测试如下,输出为 0,可知线程 t2 没有等 t1 改值就已经读到 x 了。
    public class VolatileTest {
        public static void main(String[] args) {
            Critical critical = new Critical();
            Thread t1 = new Thread(critical::read);
            Thread t2 = new Thread(critical::write);
            t1.start();
            t2.start();
        }
    }
    
    class Critical {
        private volatile int x = 0;
        @SneakyThrows
        public void write() {
            Thread.sleep(500);
            x = 1;
        }
        @SneakyThrows
        public void read() {
            Thread.sleep(300);
            System.out.println(x);
        }
    }
    
  4. 为什么规则和上述实验现象不符合呢?

    • 可能性有三点:一,规则错了;二,对规则的理解错了;三,实验有误差或设计不严谨。想想也知道,第二点可能性最大。
  5. 如果是对规则的理解不对导致实验思路就不对,那么如何理解?

    • 首先要搞清楚理解偏差在哪个地方。问题的核心是 volatile 域,原文 volatile field,到底是个什么东西。它是我们在代码里声明的那个变量吗?这里又有个理解上的问题了:“代码里的变量”是什么?稍加思索你会发现,这个“代码中的变量”在计算机里可不仅仅是一个变量!它会在内存里有一个变量,在 CPU 的缓存里有三个或者其它数量(大部分 CPU 有三级缓存,也有部分 CPU 是非三级缓存的),而且说不定这时候 CPU 正在使用这个变量,那么此时在寄存器里也有此变量。这几个变量都是由同一句代码声明而产生的变量,那么描述它们还能用“代码中的变量”这样不准确的说法吗?显然不行了。

    • 精确到目标的问题应该是: volatile 域是指内存里的那个变量,还是缓存里的那些个变量,亦或是其它? 这就需要结合语境了,volatile 规则是在 JMM 模型的大框架下建立的。JMM 模型是啥?就是用来描述在 JVM 中物理内存如何与 CPU 缓存通信,且内存中的数据如何参与 CPU 调度的模型。主角是内存,所以 volatile 域指的就是内存中的 volatile 变量。因为程序的底层调度并不像纸面代码那么简单直白,上面的实验就是如此,底层的逻辑粒度已经超过了代码能表述的粒度,所以我们只能用理论来描述代码的结果,而很难用代码来描述执行的过程。所以想仅仅站在代码的角度去理解 volatile 规则是不行的。

    • 要理解 volatile 变量规则,要直接从 JMM 模型入手。如图2(图2来自知乎 @阿里云云栖号,侵删。)

      图 2

      JMM 将底层复杂的物理硬件抽象成了简单的几层:主内存(大致对应物理上的内存条),JMM控制(大致对应存储层面的数据调度策略,由 JVM 封装好的逻辑),工作内存(各级缓存和寄存器),线程(CPU 在时钟周期内的运行动作)。

      volatile 变量在主内存中。我们知道单线程的规则是顺序规则,而多线程在使用了主内存中的 volatile 变量时,volatile 规则就体现了。先将这个模型的结构图刻进脑海中,再来看一个经典的例子,应该比较好理解:

      class Critical extends Thread {
          private boolean flag = false;
          // getter and setter
      
          @Override
          public void run() {
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              this.flag = true;
          }
      }
      
      public static void main(String[] args) throws InterruptedException {
              Critical critical = new Critical();
              critical.start();
              for (;;) {
                  if (critical.isFlag()) {
                      System.out.println("Flag is set to true");
                  }
              }
      }
      
      

      main 方法执行后,你将永远也不会看到 “Flag is set to true” 的输出(因为没有同步语句执行,所以缓存内的值暂时不会写回内存),如图3:

      图 3

      如果将 flag 改为 volatile 类型的变量会怎样呢?那就肯定会输出 “Flag is set to true” ,如图4:

      图 4

      再来对 volatile 规则加以理解:对于一个 volatile 域的,happens-before 于任意后续对这个 volatile 域的

      现在最大的问题是很容易觉得这个“写”存在歧义:一是我们的代码逻辑将线程工作内存设成 this.flag = true;二是主内存中的 flag = true。哪个才是规则里指的“写”操作呢?有了上述分析我们知道 “volatile 域” 就是主内存中的变量,于是毫无疑问这个“写”指的就是后者了。如上图,将写的时间点记为 T0。volatile 变量规则就可以翻译为 “T0 以后的读操作都在 T0 以后进行,比如 T1 在 T0 以后进行”。

      ???

      好家伙,在这原地 TP 呢!听君一席话如听一席话是吧!我上次听到这么牛逼的话还是在上次!

      搞了半天这个规则只想告诉我们一句牛逼的废话吗?就如同没有这句话,程序那样运行依旧是天经地义的。肯定还有哪里不对!

    • 结论修正。回过头再想, volatile field 这个词是否还有其它含义?显然是有的,在 Java 的世界观中,本地变量就叫 field,那么其实线程的工作内存里的变量叫 volatile field 好像也没问题。这样的话,将图 3 与图 4 的第 4 步都认为是“写”操作,区别就出来了:普通变量的写操作对主内存后续的读操作无影响,而 volatile 变量的写操作对后续主内存的读操作有影响!

      归纳一下,便得到一个结论:不考虑同步的前提下,普通变量的写操作对主内存后续的读操作无影响,而 volatile 变量的写操作对后续主内存的读操作有影响。详细的影响是:记 volatile 变量的写操作真正生效时间为 T0,那么 T0 时间点后的其它对主内存的读操作读到的肯定是写后的值。

      我想这才是 volatile 变量规则想说的真正意思。

      对规则咬文嚼字一下,得到图 5:

      图 5

      其实一般不需要像个强迫症一样每个字都抠出含义的,感觉能理解到上面的结论就差不多了。这点搞清楚然后再去讨论 volatile 变量规则是如何确保的,这才引出了 volatile 关键字的底层实现原理——内存栅栏(或者叫内存屏障)。不过这部分内容并不是什么很难理解的东西,就不在这里讨论了,翻翻书就明白了。

扩展式理解

至今看到不少文章都能将原理侃得很深了,但是缺少了现实的承载,显得抽象感极强,什么 CPU,缓存,主内存,令人云里雾里。在此我就把理论中经常提到的东西列出来,好让大家明白我们平时经常说的一些东西究竟是什么。

以我用的 AMD 5000 系列的 CPU 为例。上结构如下图。

日常使用的CPU(听说最近新的 DDR5 内存的 MC 移到了内存条上):

图 6

CPU 中的一个 CCD 封装:

图 7

缓存的架构:

图 8

大致了解了 CPU 的架构后你会发现,上层应用屏蔽了多少复杂的东西啊!

结合上面说过的:

  • JMM 主内存——内存条
  • JMM 控制——数据调度策略(MC,MB,Cache Controller 等硬件与软件结合而实现)
  • JMM 工作内存—— CPU 中的各级缓存(如核外 L3,核内的 L1,L2)
  • JMM 中的线程—— CPU 内核里运算器在时钟周期内的任务执行过程

了解完硬件结构后是不是对软件有了更多的认识呢?

JMM 是一个蓝图,我们的 volatile 规则是这个蓝图里的一个条目,它就是基于这样的硬件条件,由 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

推荐阅读更多精彩内容