线程竞态条件:静态整数num的最小与最大取值范围疑问
为什么静态变量num的最小值可以低于5?
嘿,这个问题确实容易踩坑!你说的最大值25是完全正确的——当所有num++操作都顺顺利利没有竞态,每个线程的五次自增都依次执行、互不干扰时,5个线程×5次自增=25,最终num就是25。
但关于最小值,面试官说的没错,它确实可能低于你认为的5,核心原因在于两个关键点:num++不是原子操作,以及**num没有可见性保证**。
先搞懂num++到底在干啥
num++看起来是个单一操作,但在JVM层面其实拆成了三步:
- 把当前
num的数值读到寄存器里 - 把寄存器里的值加1
- 把新值写回内存
这三步之间完全可以被其他线程打断,比如5个线程同时读到num=0,然后各自加1变成1,再依次写回内存——这5次操作最后只让num从0变成1,等于白做了4次。
更关键的:num没加volatile,线程之间看不到彼此的修改
因为num没有被volatile修饰,Java内存模型不强制线程之间的变量可见性:
- 线程A修改了
num,线程B可能永远看不到这个变化——线程B可以一直用自己缓存里的旧值,完全不主动从主存刷新。 - 注意:线程自己的修改在自身线程内是可见的(比如线程A执行五次自增,它自己肯定能看到
num从0涨到5),但其他线程完全可以无视主存里的最新值。
举个极端例子:最终num=1
给你讲一个极端但完全符合Java内存模型的场景:
- 初始时主存里
num=0,所有5个线程都把0加载到自己的缓存里。 - 线程1先执行第一次
num++:读0→加1→写1到主存,这时候主存num=1,但线程2到5的缓存还是0,它们没主动刷新,所以根本看不到这个变化。 - 线程2到5接着执行第一次
num++:都读自己缓存里的0,加1变成1,然后写回主存——每次写回都会把主存的1覆盖成1,等于第一次循环结束后num还是1。 - 第二次循环开始:线程1读自己缓存里的1(自己的修改自己能看到),加1变成2写回主存;但线程2到5还是读缓存里的0,加1变成1写回,又把主存的2盖回1。
- 第三次到第五次循环全是这个套路:每次线程1好不容易把
num改成2,其他线程马上给盖回1。最后五次循环结束,num=1,这不就远低于5了嘛。
甚至还有更极端的情况:如果所有线程在整个五次循环里都死死抱着缓存里的0不放,每次自增都写回1,那最终num就停在1——这完全合规,因为Java没有规定线程必须定期刷新缓存去读主存的最新值。
总结一下:没有可见性保证的情况下,线程之间的修改可以完全互不感知,导致大量自增操作被无效覆盖,num的最小值理论上可以低到1。
内容的提问来源于stack exchange,提问作者Anmol Singh Jaggi
相关产品推荐
相关产品推荐

