求助>一道面试题引发的对Java内存模型的一点疑问?>
27回复
1年前

一道面试题引发的对Java内存模型的一点疑问?



image.png

如上图所示程序,按道理,子线程会通过num++操作破坏while循环的条件,从而终止循环,执行最后的输出操作。

但在我的多次运行中,偶尔会出现while循环一直不结束的场合。像我截图一样,程序一直不终止。

JDK7、JDK8均已试验,均能偶然触发。

3629 阅读
请先登录,再评论

回复列表

sofkyle1年前

答案由亮哥提供:
https://www.zhihu.com/question/263528143/answer/270308453

简单说来,是因为JIT激进编译导致的问题。

volatile只是恰巧阻止了JIT的激进编译,所以这里主要的问题不是可见性。因为哪怕变量不是volatile修饰,只要加上-Xint、-XX:-UseOnStackReplacement参数,问题一样不会出现。

2
https://a.perfma.net/img/2382850
burnbrid5月前

你这个问题绝对跟JIT的激进编译没有关系,就是num变量的可见性问题,JMM的规范:每个线程都有自己的工作内存。
第一:你说的JDK7、JDK8均已试验,均能偶然触发。为啥偶然触发呢?是因为一旦Main线程在num=0时候先执行了while()循环,然后子线程再对num进行加1操作,就会出现死循环的情况。你说的偶然就看Main线程跟子线程谁先运行了。就像你评论里面回复别人的 ‘在num++前面加行Thread.sleep(1000)。’,你发现没?在num++前面加了Thread.sleep(1000)就百分之百会出现死循环,这是为啥呢?就是因为Main线程在num=0时候先执行了while()循环,然后子线程再对num进行加1操作,就会出现死循环的情况。
第二:为啥加了-Xint、-XX:-UseOnStackReplacement参数,问题就不会出现了?这个评论里面的人已经说出来了,加了-Xint解释执行情况下getstatic指令是从内存读取值的,所以Main线程每次循环的时候都是从主内存读值的,子线程修改了num的值,Main线程可以读到,所以加了-Xint解释执行肯定不会出现死循环。

https://a.perfma.net/img/2382850
burnbrid5月前

按照我的理解,你这个程序如果Main线程在num=0时候先执行了while()循环,然后子线程再对num进行加1操作,肯定会出现死循环的情况。因为JMM规定普通的共享变量存在于主内存当中,然后每个线程都有自己的工作内存,每个线程用到变量的时候会先从主存中复制一份到自己的工作内存。就你这程序来说,如果Main线程在num=0时候先执行了while()循环,这个时候Main线程会把num的值复制一份到自己的工作内存,然后Main线程的while循环只会读线程工作内存的副本。接下来子线程开始运行,先把num的值复制一份到在子线程的工作内存里面,然后对num进行加1操作,然后子线程运行结束把num刷新到主存里面。注意,即使此时子线程把num的最新值刷新到主存中,Main线程的while循环也不会结束,因为Main线程只会读自己的工作内存里面num的值,Main线程的工作内存里面的num现在还是0啊。你的num没有用volatile修饰,所以Main线程里面的num不会失效。所以,我觉得你这个程序只要Main线程在num=0时候先执行了while()循环,然后子线程再对num进行加1操作,即使没有jit的激进编译,也肯定会出现死循环的情况。各位大佬,看下我的评论,不知道我说的对不对?

2
sofkyle4周前
回复 burnbrid:

你要考虑一个问题,工作线程会用自己的缓存,这没问题。问题的关键在于,工作线程使用的缓存,是总会刷到内存上去的。可以查查资料,我现在找不到了,相关资料的描述是,对于工作线程使用的缓存刷到内存上的时机是不可预测的。虽然是不可预测的,但是总会有那么一个时机会触发。
你可以运行试试,当触发了循环之后,永远不会终止,那么就说明,即便缓存上的数据刷上内存,仍跳不出循环。

回复
回复 burnbrid:

如下为 main 线程卡主的地方, 我这里分别测试了两种情况

    // 循环内部为 ";", 什么都没有
//    (lldb) thread select 5
//            * thread #5
//    frame #0: 0x000000010d2f155f
//            ->  0x10d2f155f: testl  %eax, -0xcf80565(%rip)
//            0x10d2f1565: jmp    0x10d2f155f
//            0x10d2f1567: movl   %r10d, %edi
//    0x10d2f156a: negl   %edi

    // 循环内部为 counter ++
//    (lldb) thread select 5
//            * thread #5
//    frame #0: 0x000000010c4e9b45
//            ->  0x10c4e9b45: incl   %r13d
//    0x10c4e9b48: testl  %eax, -0xc278b4e(%rip)
//            0x10c4e9b4e: jmp    0x10c4e9b45
//            0x10c4e9b50: movl   %r11d, %r10d
回复
回复 burnbrid:

能够拿出运行时的数据信息么, 否则只是理论推导(你理解的理论是可能存在偏差的), 没有说服力的, 我更倾向于 知乎的这篇回复

回复
查看更多
._3762361年前

这样应该是变量可见性的问题

徐小白1年前

尝试跑了几遍,没跑出图中的结果。是不是还跟操作系统有关系的?

sofkyle1年前
回复 徐小白:

可以试试在num++前面加行Thread.sleep(1000)。

回复
瞭望1年前

按道理,子线程使用了system.out输出,system.out内部采用了syncronized同步,会触发num变量的主存同步,所以为什么还会出现死循环,感觉跟不同cpu mesi协议有关,可能mesi协议本事支持写入缓存未达到失效队列上限,所以导致主线程读取的num一直没有被失效

sofkyle1年前
回复 瞭望:

有相关资料能提供下吗,关于syncronized同步,会触发num变量的主存同步。以及mesi协议的失效队列?

回复
大空翼1年前

加一个-Xint再试试看😁

1
sofkyle1年前
回复 大空翼:

加-Xint、-XX:-UseOnStackReplacement都能解决问题。

回复
sofkyle1年前

我晕,忘记调度问题了。。。如果子线程先执行num++,那么主线程while循环就直接退出了。被带偏了。

Balloon1年前
回复 sofkyle:

主要还是volatile的问题

回复

变量前加个 volatile

1
sofkyle1年前
回复 亮哥:

这篇文章不错,感觉解决了我的问题。想点这个采纳的,但是发现回复不能采纳。😂

回复
亮哥1年前
回复 sofkyle:

有个更好的例子 在别的变量加volatile https://www.zhihu.com/question/263528143/answer/270308453

回复
亮哥1年前
回复 sofkyle:

因为jit优化导致的 while 会变成true ,解释执行情况下或关闭jit优化则不会进行优化 而且解释执行情况下getstatic指令是从内存读取值的。所以是始终可见,但因为cpu重排关系有可能不是实时可见 ,不好测

回复
查看更多