Java是否允许无界饥饿?volatile循环程序的JMM合规性问询
引言
考虑以下Java程序:
public class Loop { static volatile boolean flag; public static void main(String[] args) { new Thread(() -> flag = true).start(); while(!flag); } }
简单描述:存在两个线程,一个线程在循环中等待volatile boolean flag变为true,另一个线程将flag设为true。
我认为Java语言规范(JLS)并未保证该程序会终止,以下是我的推理过程。但由于类似程序有时被作为Java同步的正确示例,我不确定自己的理解是否正确。
推理过程
所有对JLS的引用均基于Java SE 23。为简洁起见,若未明确指定变量,提及的读/写操作均指对flag的读/写。
§17.4定义了Java内存模型(JMM),用于划分程序的合法与非法行为。§17.4.1至§17.4.7规定了合法执行的概念;§17.4.8进一步约束执行需满足因果性要求;§17.4.9基于合法执行定义了程序的可观测行为。
合法执行
首先,我认为对于任意n > 0,该程序存在一个包含恰好n次volatile读操作的合法执行,且无外部操作(除§17.4.9引入的executionTermination操作)。具体而言,该执行的同步操作按同步顺序排列如下:
- 两个线程A和B启动(即执行其合成初始操作;参见§17.4.2)。
- A执行
n-1次volatile读操作,读取到默认值false的合成写入。 - B执行一次volatile写操作,将值设为
true。 - A再执行一次volatile读操作,读取到B执行的volatile写操作的值。
- A和B均终止(即执行其合成最终操作;参见§17.4.2)。
我认为这些执行满足合法执行的所有要求及因果性条件,因数学论证繁琐在此省略。
可观测行为
接下来,我将论证仅包含hang操作的行为是该程序的可观测行为。
§17.4.9规定:
[...]
若O是执行E的可观测操作集,则O必须是E的操作集A的子集,且即使A包含无限个操作,O也只能包含有限个操作。此外,若操作y在O中,且存在hb(x, y)或so(x, y),则x也必须在O中。
[...]
行为B是程序P的允许行为当且仅当B是有限的外部操作集,且满足以下任一条件:
- [...]
- 存在操作集O,使得B由一个hang操作和O中的所有外部操作组成,且对于所有k ≥ |O|,存在P的执行E(操作集为A),以及操作集O',满足:
- O和O'均为A的子集,且满足可观测操作集的要求。
- O ⊆ O' ⊆ A
- |O'| ≥ k
- O' - O不包含任何外部操作
选择B为仅包含hang操作的单元素集,O为空集。对于任意给定的k,选择E为包含恰好k次读操作的执行,O'为A去掉executionTermination后的集合,可满足所有条件。
实际JVM的行为
我在x86_64机器上使用OpenJDK(Oracle build 23+37-2369)进行了实验。只要flag是volatile,无法让程序挂起;仅当flag为非volatile时,C2会将循环优化为无限循环。
技术问询
- 我对JLS/JMM的理解是否正确,即上述程序允许不终止?若不正确,我的错误在哪里?
- 若我的理解正确,是否存在特定环境组合(CPU架构、JDK版本、JVM参数等),使得上述程序或其
flag为volatile的变体在实际JVM上不终止?
内容的提问来源于stack exchange,提问作者Haspamelodica

