Java指令重排序与volatile关键字

Java指令重排序与volatile关键字

1. 重现代码重排序

1.1 测试代码

完整代码参见Github,其中关键代码如下:

Thread thread1 = new Thread(
        () -> {
            a = 1;
            y = b;
        }
);
Thread thread2 = new Thread(
        () -> {
            b = 1;
            x = a;
        }
);

1.2 理论推断

因为thread1和thread2都join到当前线程,则代码执行到这里以后,两个线程都执行完毕,因为多线程的原因,代码执行顺序不同,理论上xy的值可能为(1,0)(0,1)或者(1,1),分别对应如下的执行顺序(从上到下)

  • 1,0的情况
线程1 线程2 x y
a=1; 0 0
y=b; 0 0
b=1; 0 0
x=a; 1 0
  • 0,1的情况
线程1 线程2 x y
b=1; 0 0
x=a; 0 0
a=1; 0 0
y=b; 0 1
  • 1,1的情况
线程1 线程2 x y
b=1; 0 0
a=1; 0 0
x=a; 1 0
y=b; 1 1

1.3 指令重排序导致的特殊情况

实际运行中,运行上述的代码足够长的时间后,会有某个线程进入错误分支,打印如下错误并关闭线程池

Wrong,x = 0 and y = 0
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@d716361 rejected from java.util.concurrent.ThreadPoolExecutor@3764951d[Shutting down, pool size = 12, active threads = 12, queued tasks = 23280, completed tasks = 2713]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
    at concurrent.reorder.Reveal.main(Reveal.java:59)
Wrong,x = 0 and y = 0

表明实际运行中会有两个线程都执行完毕,然而x和y都是0的情况,这时就是发生了指令重排序,即代码运行的顺序,与源代码的顺序不一致,具体到测试代码,即可能实际运行顺序如下

  • 0,0的情况(发生指令重排序)
线程1 线程2 x y
y=b; 0 0
x=a; 0 0
a=1; 0 0
b=1; 0 0

此时最终打印x=0;y=0;

2. 什么是指令重排序

2.1 Java源代码到运行时指令

Java编译出来的class文件,仅能被Java虚拟机(JVM)识别,实际在运行时,会由实际运行的JVM编译成机器码运行,粗浅的理解为:Java.class文件 -> JVM运行时解析为机器码 (-> JIT优化过后的机器码) -> 操作系统的CPU指令,其中JVM解析为机器码、JIT优化成机器码,CPU执行CPU指令的过程中均有可能发生指令重排序

2.2 宿主机的内存模型与变量操作

所谓宿主机,即运行JVM的机器,可能是个人开发的电脑,线上的生产服务器,Docker容器等,操作系统、硬件的不同,内存模型和指令也不尽相同,鉴于目前多核CPU无论在开发环境和生产环境均为主流,一般认为宿主机的内存模型简化为主内存和多级CPU内部缓存再到寄存器,简化后的模型如下

寄存器 <-> CPU内部共享高速缓存(L1\L2\L3 Cache) <-> 主内存(RAM)

高速缓存仅仅是用作寄存器和主内存之间缓存用,CPU通过各种技术保证寄存器读取时缓存内的值与主内存的对应值一致,因此进一步简化为

寄存器 <-> 内存(Cache和RAM)

在此简化模型下,假设当前主内存中一个变量x初始值为0,一个简单的赋值操作a=0;x=a+1的执行顺序如下

  1. 从内存读取a的值0到CPU Processor的寄存器,并赋予临时地址r1,可看作 r1 = a;
  2. 寄存器内累加r1 = r1 + 1;
  3. r1的值写回内存,x=1

假设MOV [v1, v2]代表v2变量复制到v1变量,S1表示Step1,r开头的变量表示寄存器变量,则上述步骤简写为

  • S1: MOV [r1, a]
  • S2: MOV [x, ++r1]

执行顺序为 S1 -> S2 ,后面也按此约定说明

2.3 CPU指令重排序

2.3.0 测试程序

将1.1中的测试程序改写为

public Test{
    int a = 0,b = 0,x = 0,y = 0;

    void test1(){
        a = 1;
        x = b;
    }

    void test2()[
        b = 1;
        y = b;
    ]
}

则按照2.2的写法,将test1方法内部CPU指令简写为:

  • S1: MOV[a, 1]
  • S2: MOV[r1, b]
  • S3: MOV[x, r1]

test2方法内部CPU指令简写为:

  • S4: MOV[b, 1]
  • S5: MOV[r2, a]
  • S6: MOV[y, r2]

后面的程序均围绕此程序展开

CPU指令重排序的定义为:CPU允许在某些条件下进行指令重排序,仅需保证重排序后单线程下的语义一致,这句话比较绕口,其中有三个加粗后的关键字,具体解释如下:

2.3.1 某些条件

我们把变量读到寄存器的操作称为Load,把变量从寄存器写出到内存的操作称之为Store,则下面的操作称之为Store-Load操作:

  • MOV[r1, x]
  • MOV[y, r1]

类似的还有Load-Load,Load-Store,Store-Store操作,对于这几种操作,Intel规定Store-Load操作,且Store中涉及到的外存变量与Load中涉及到的外存变量不同的情况下,可以发生指令重排序,当然对于不同的CPU、指令集,可重排序的指令不同,一般情况下认为大多数CPU均支持Store-Load重排序,具体的支持操作请参考最后的参考资料或自行查阅相关网站

2.3.2 指令重排序

假设一个线程执行2.3.0中程序的test1()方法,由于S1为Store指令,S2为Load指令,且涉及的外存变量不同,根据2.3.1的说明,允许发生重排序,即允许指令执行顺序为S2 -> S1 -> S3,注意,由于S3语句中r1的值只跟S2位置有关,因此,重排序后的语句执行效果类似x=b;a=1;,类似的test2()方法可被重排序为S5 -> S4 -> S6,执行效果看上去像y=a;b=1;,注意,这里的看上去像仅仅是指最终执行顺序看上去的样子

2.3.3 重排序后单线程下的语义一致

如果仅有一个线程顺序执行test1()和test2()方法,正常执行的结果为a=1;b=1;x=0;y=1;即使指令被重排序为S2 -> S1 -> S3 -> S5 -> S4 -> S6,最终执行结果仍旧为a=1;b=1;x=0;y=1;,与源码中直接推导或者说重排序前的执行结果是一致的,这就叫做重排序后单线程下的语义一致

2.3.4 指令重排序与多线程程序

2.3.3中阐明了,指令重排序对于单线程程序没有影响,但是假如有两个线程分别运行test1()方法和test2()方法,假设发生指令重排序,由于多线程程序执行顺序的不确定性,可能的一种执行顺序为:

线程1 线程2 r1 r2 x y a b
S2( MOV[r1, b] ) 0 - 0 0 0 0
S5( MOV[r2, a] ) 0 0 0 0 0 0
S1( MOV[a, 1] ) 0 0 0 0 1 0
S3( MOV[x, r1] ) 0 0 0 0 1 0
S4( MOV[b, 1] ) 0 0 0 0 1 1
S6( MOV[x, r2] ) 0 0 0 0 1 1

在这种情况下,最终x=0;y=0;这就是1.3中出现反直觉的结果的原因,最终展现出的效果就类似下面的表格,看上去是两个线程的代码进行了重排序

线程1 线程2 x y
y=b; 0 0
x=a; 0 0
a=1; 0 0
b=1; 0 0

3. 如何避免多线程程序中指令重排序造成的错误

3.1 Java内存模型(JMM)

为了保证JVM的跨平台性,把Java业务代码与操作系统或硬件的指令解耦,JSR规定了一系列Java代码在多线程程序中与内存交互中的原则,如happens-before原则,serial-as-if原则,JVM实现必须遵循这些原则,同时,没有在JSR133中禁止的指令重排序、优化等等均是被允许的

3.1.1 JMM的happens-before原则

如果两个动作符合happens-before原则,则两个操作互相间指令重排序受到限制,如果一个动作happens-before另一个动作,则第一个对第二个可见,且第一个排在第二个之前

  • 一个线程的各个action happens-before 这个线程的subsequent action
  • 一个monitor的unlock happens-before 这个monitor的subsequent lock
  • 对一个volatile变量的write happens-before 这个变量的read
  • 对一个线程的start()操作happens-before开启的线程里的action
  • 一个线程的所有action happens-before 其他join这个线程的action
  • happens-before有传递性,即如果a happens-before b,b happens-before c,则a happens-before c

3.2 volatile关键字

JMM对于volatile关键字的规定,可以归结为两层:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
2)当指令进行到volatile变量的Store操作时,在此之前的所有指令必须执行完毕,且在此之后的指令尚未执行

3.2.1 volatile关键字对指令重排序的影响

从3.1.1可知,JMM规定如果一个变量被volatile修饰,则Store-Load操作不会被指令重排序

3.2.2 验证volatile关键字对内存的影响

将1.1中测试代码里的变量a,b用volatile修饰,则无论运行多久,都不会再出现x=0;y=0;的情况,但仅修饰a和b其中一个不会有此效果

3.2.3 验证代码解析

对volatile变量a,b的操作S1和S2之间,因为S1是volatile变量a的Store操作,因此S1不可和S2进行重排序,类似的,S4和S5也不可进行重排序,这就避免了2.3.4中重排序后指令的执行可能,因而不会出现x=0;y=0;的情况

4 参考

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

推荐阅读更多精彩内容