Volatile趣谈——我是怎么把贝克汉姆的进球弄丢的

大雄的门线传感器

大雄的公司最近在给国际足球协会研制一款门线传感器。这可是一个大单,组织特地安排了大雄作为首席程序员,来开发这款软件。
需求很简单,传感器需要在皮球越过门线的时候,给裁判身上的耳麦发送消息,告诉裁判球进了。
“So easy”,大雄四两拨千斤地写了一个进球通知线程:

public class GoalNotifier implements Runnable {
    public boolean goal = false;

    public boolean isGoal() {
        return goal;
    }

    public void setGoal(boolean goal) {
        this.goal = goal;
    }

    @Override
    public void run() {
        while (true) {
            if (isGoal()) {
                System.out.println("Goal !!!!!!");

                // Tell the referee the ball is in.
                // ...

                // reset goal flag
                setGoal(false);
            }
        }
    }
}

“只要在比赛一开始就启动这个线程,然后当球越过球门线的时候,调用我的setGoal()方法,把进球标志goal设置成true就OK了”,大雄对着投影里的代码,跟产品经理胖虎讲解着自己伟大的设计。
“很棒!代码写的非常简洁,设计非常优雅,连我都看不出有什么Bug。静香,不用浪费时间测试了,直接上线吧,时间就是金钱,我们要敢在别的竞争对手之前,推出这款产品!”,胖虎激动的说,唾沫横飞。
“好的,我也相信大雄的能力!”,静香含情脉脉的看着大雄,眼里都是崇拜。

Oop! Bug!

很快,大雄的门线传感器上线了。英格兰足协老总约翰是个很喜欢新科技的人,他迫不及待地想把这项技术推广到他的国家。
这天,有一场让世界瞩目的友谊赛——曼联传奇队 vs 阿森纳传奇队。“把我们刚刚买过来的门线技术用上去,让这群老家伙见识一下什么是高科技!”,约翰说。

比赛开始,刚开场,只见鲁尼把球往旁边一拨,贝克汉姆就顺势一脚圆月弯刀,皮球划出一道美丽的弧线,飞过大半个足球场,阿森纳门将始料不及,只能目送皮球应声入网!

这个过程之迅猛,只能用下面这段代码来描述了:

public class Game {

    public static void main(String[] args) throws InterruptedException {
        // Game begun! Init goalNotifier thread
        GoalNotifier goalNotifier = new GoalNotifier();
        Thread goalNotifierThread = new Thread(goalNotifier);
        goalNotifierThread.start();

        // After 3s
        Thread.sleep(3000);
        // Goal !!!
        goalNotifier.setGoal(true);
    }

}

就在曼联队队员抱在一起庆祝的时候,裁判跑了过来,宣布进球无效,原因是门线传感器没有提示他进球了。。。
“What ???”,贝克汉姆一脸懵逼。。。
“大雄,怎么回事???”,约翰气冲冲的对旁边的大雄说。
“啊,难道是线程没起来吗?”,大雄也是一脸懵逼,“我加了日志的,看一下后台就知道了!”
于是大雄登录了后台服务器,查看了日志信息:

“啊,一行日志都没有。。。”,大雄很慌,“看来只能求助哆啦了。。。”

大雄赶紧视频了正在日本度假的哆啦,视频里,哆啦一边喝着大阪清酒,一边看着大雄的代码,大概过了十秒钟,突然挂断了视频。
“难道连哆啦也没有办法了。。”,就在大雄绝望的时候,他突然收到哆啦发来的信息,打开一看,里面就一个词:
volatile

“啊,难道是它。。。”,来不及想太多了,贝克汉姆随时都会再进球,“不能让我贝失望啊”,大雄赶紧改了一行代码:

public class GoalNotifier implements Runnable {
//    public boolean goal = false;
    public volatile boolean goal = false;
    ...  

刚改完代码,这边曼联队就得到一个禁区外任意球的机会,贝克汉姆一记招牌的圆月弯刀,皮球直挂死角!不过这次,大家都没庆祝,而是一致看向了裁判,全场鸦雀无声。。。
突然,主席台那里,有一个像逗比一样的青年,大声的吼着,“Yeah!!! 日志打印出来了!!!”,声音之大,响彻全场。

过了大概两秒钟,人们才看到裁判把手指向了中圈,示意进球有效。。。

volatile和Java内存模型

“为什么把goal变量加上volatile修饰符,问题就解决了呢?”,带着这个疑问,大雄开始研究了起来。渐渐的,他认识到,看Java代码,不能只看表象,还要透过Java虚拟机,去看透本质。从JavaSE到JVM,这是一场认知的跃迁

首先要解决的问题是,不加volatile之前,main函数明明调用了setGoal()方法,把goal改成了true,可为什么GoalNotifier线程里的goal还是false?
答案是,主线程里调用setGoal()方法修改的goal,和GoalNotifier线程里的goal,是两个副本
What??? 变量还有副本?
单看代码,自然是看不出“副本”的,我们必须剥开代码这层皮,到Java虚拟机里头去看看。

在介绍JVM中的“副本”之前,我们先来简单聊聊物理机的“副本”,因为JVM的副本和物理机的副本很像。
计算机,相比于处理器的运算速度,IO操作的速度往往有几个数量级的差距,因此像下面这段常见的++运算:

int count = 0;
...
count ++;

如果计算机把count的值存储在内存中,那么每次++操作,就有一次从内存中读取i的值的操作,以及一次把i的值加1的操作,别忘了,还有一次把i的值写进去内存的操作:
T(一次循环) = T(读IO) + T(+1运算) + T(写IO)
而IO操作的速度往往比运算速度多几个数量级,所以:
T(一次循环) ≈ T(读IO) + T(写IO)
显然,IO操作的速度严重拖后腿了,不管运算速度再快,只要IO操作还在,这个++操作的速度就永远由IO操作的速度决定
我们人类自然不允许这样的情况发生,因此我们在处理器和内存之间,引入了读写速度接近处理器运算速度的一层高速缓存

CPU、高速缓存和内存

这样,在上面的++操作里面,count变量只有在初始化的时候,需要写入主内存,接着,count就被从主内存拷贝到处理器的高速缓存中,下次再想对它执行++操作时,直接从高速缓存中读取就可以了,++操作执行完之后,也不需要马上同步到主内存。

虽然各种平台都会有高速缓存和主内存,但是不同平台的内存模型并不完全相同。这也就导致了像C/C++这种直接使用物理机内存模型的编程语言,有时候一份代码在一个平台上可以正常运行,去到另一个平台就挂了,所以需要“面向平台”编程。而Java,正如广告语说的,“Write once, run anywhere”,相同的一份代码,去到哪个平台都可以直接拿过去用。
为什么Java这么神奇呢?这自然是JVM的功劳,你下载JDK的时候,会让你选择是Windows还是Linux的。使用不同平台的JDK,最大的差异就是JVM了,相同的一份代码,Windows版的JVM帮你把代码翻译成Windows系统能识别的机器语言,Linux版的JVM则翻译成Linux的语言。
JVM帮你屏蔽了不同平台直接的差异
自然的,对于物理机的内存模型,JVM也要进行“介入”,我们编写的Java代码,是不会直接去操作物理机的内存的,而是去操作JVM定义的Java内存模型(Java Memory Model, JMM),再通过JMM去操作物理机的内存。
Java的内存模型和上面讲的物理机的内存模型非常类似:

Java内存模型

现在再回过头来看大雄碰到的问题:main函数明明调用了setGoal()方法,把goal改成了true,可为什么GoalNotifier线程里的goal还是false?
答案已经很明确了,这里面有两个线程,main函数所在的是主线程和GoalNotifier线程,这两个线程都分别从主内存从拷贝了一个goal变量的副本,所以当main函数调用setGoal()方法修改goal时,修改的其实是自己线程工作空间上的那个副本goal,对主内存的goal没有影响,对GoalNotifier线程的goal副本更加没有影响,GoalNotifier线程自然就感知不到goal变成true了。

那么,要怎样才能让GoalNotifier线程,能够感知到main函数修改了goal呢?
很简单嘛,让main函数修改了goal之后主动同步到主内存,并且让GoalNotifier线程在读取goal的之前,主动从主内存去取goal,事实上,这就是volatile的原理。

volatile的内幕

那么volatile是如何让修改的变量立刻同步到主内存的呢?
同样,单看代码是看不出来的,volatile只是我们告诉JVM的一个标志,那么JVM对于有volatile和没有volatile的代码,在翻译成机器指令时,会有什么不同呢?

有同学会建议用javap命令反汇编查看一下,如果你也这么想,那现在我直接告诉你,不可以,至于为什么,你可以先自行研究,我将在后面单独用一篇文章讨论。
在这里我们要使用JIT级别反汇编命令,原因同样不在这里赘述。下面简单介绍一下方法。
加入如下虚拟机参数:

-XX:+UnlockDiagnosticVMOptions -Xcomp -XX:+PrintAssembly -XX:CompileCommand=compileonly,*GoalNotifier.setGoal

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly:开启JIT反汇编
-Xcomp:让虚拟机以编译模式执行代码,使得JIT编译可以立即触发
-XX:CompileCommand=compileonly,*GoalNotifier.setGoal:只反汇编GoalNotifier的setGoal方法

然后执行两次代码,一次加入volatile修饰符,一次不加,把两次控制台打印的汇编语言,放到文件对比工具上对比一下,打印的信息很多,但是通过文件对比工具,我们可以很清楚的看到,加了volatile的代码中,多了一行代码:



这行“lock add dword ptr”的代码是干什么用的呢?关键在于lock,这个lock不是指令,而是指令前缀,我对汇编语言不熟悉,这里借用《深入学习Java虚拟机》里的解释:“lock的作用是使得本CPU的Cache写入内存,同时使其他CPU的Cache无效”,其实也就是我们上面讲的,将修改后的变量主动同步到主内存。

加了虚拟机参数后,运行的时候你可能会看到错误提示,别慌,很容易解决。另外,我把我做实验生成的两份汇编语言以及其他代码上传到Github了,有兴趣的同学可以下载下来研究。

总结

对于volatile这个关键字,可能大家都听过很多遍,但是由于实际中很少用到,所以大多不太了解其背后的原理。这次通过对volatile的介绍,顺带讲解了Java内存模型,同时也看到了Java虚拟机在Java中的扮演的地位,还是那句话,看Java代码,不能只看表象,还要透过Java虚拟机,去看透本质。从JavaSE到JVM,这是一场认知的跃迁

这篇文章与其说是讲volatile,不如说是讲JVM。对volatile的介绍也只提到了它在可见性上的作用,volatile的另一个作用——禁止指令重排,并没有提及,毕竟指令重排是个很高深的家伙,我也将在后面的文章中和大家一起探讨。

祝大家春节快乐!

参考

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

推荐阅读更多精彩内容