JCStress多读场景出现FORBIDDEN结果,请求技术分析
问题场景
单写线程先给普通变量value赋值,再修改volatile修饰的state;两个读线程按「先读state、再读value」的顺序执行。单读时无异常,但多读场景下出现了state=1(已完成)但value=0(未记录结果)的禁止结果。
测试代码
public class RequestResultTest { @JCStressTest @Outcome(id = "1, 12345", expect = Expect.ACCEPTABLE) @Outcome(id = "0, 0", expect = Expect.ACCEPTABLE) @Outcome(id = "0, 12345", expect = Expect.ACCEPTABLE) @Outcome(id = "1, 0", expect = Expect.FORBIDDEN, desc = "State is finished, but not result recorded") @State public static class SingleWriterMultiReaderTest { private final SomeResult underTest; public SingleWriterMultiReaderTest() { this.underTest = new SomeResult(); } @Actor public void finish() { underTest.finish(12345L); } @Actor public void getStateAndResult1(IJ_Result result) { int state = underTest.getState(); long longResult = underTest.getValue(); result.r1 = state; result.r2 = longResult; } @Actor public void getStateAndResult2(IJ_Result result) { int state = underTest.getState(); long longResult = underTest.getValue(); result.r1 = state; result.r2 = longResult; } } public static final class SomeResult { public static final int RUNNING = 0; public static final int FINISHED = 1; private volatile int state; private long value; public SomeResult() { this.value = 0L; this.state = RUNNING; } void finish(long value) { this.value = value; this.state = FINISHED; } public long getValue() { return value; } public int getState() { return state; } } }
测试输出
Compilation: split finish: Interpreter getStateAndResult1: C1 getStateAndResult2: C2 JVM args: [-Dfile.encoding=UTF-8, -XX:-UseBiasedLocking, -XX:+StressLCM, -XX:+StressGCM, -XX:+StressIGVN, -XX:+StressCCP, -XX:StressSeed=1773949580] Fork: #1 RESULT SAMPLES FREQ EXPECT DESCRIPTION 0, 0 142,382,593 97,91% Acceptable 0, 12345 116,937 0,08% Acceptable 1, 0 7,940 <0,01% Forbidden State is finished, but not result recorded 1, 12345 2,908,741 2,00% Acceptable
原因拆解
1. 编译器激进优化打破顺序预期
写线程中this.value = 12345(普通写)程序顺序早于this.state = FINISHED(volatile写),且JMM规则禁止普通写被重排到volatile写之后,写线程内部操作顺序是可靠的。
但C2全优化编译器会做推测加载:读线程可能提前读取value(普通变量读取无副作用),之后才读取volatile的state。如果在推测读取value=0后,写线程完成了value和state的赋值,读线程最终会返回state=1和提前读取的value=0,形成禁止结果。
2. 多线程编译层次差异放大问题
测试输出显示写线程在解释器执行,两个读线程分别由C1、C2编译。C2的优化强度远高于C1,单读场景下编译器优化较弱,因此未触发该问题;多读场景下C2的激进优化导致了异常结果。
修复方案
方案1:将value声明为volatile
让value的读/写都具备volatile语义,禁止推测加载,同时保证多线程可见性:
private volatile long value;
方案2:用同步块保证原子性与可见性
在finish方法中加入synchronized,确保写操作原子性,通过锁的happens-before规则保证读线程能看到最新值:
void finish(long value) { synchronized(this) { this.value = value; this.state = FINISHED; } }
方案3:在getValue中依赖volatile读语义
先读取volatile的state,强制建立happens-before关系,禁止value的读被重排到state读之前:
public long getValue() { // 先读volatile变量,触发缓存刷新 int s = state; return value; }
方案4:用不可变对象替换普通变量
将value和state封装为不可变对象,通过volatile引用更新,保证读线程看到的是完整的状态:
public static final class SomeResult { public static final int RUNNING = 0; public static final int FINISHED = 1; private volatile ResultHolder holder; public SomeResult() { this.holder = new ResultHolder(RUNNING, 0L); } void finish(long value) { this.holder = new ResultHolder(FINISHED, value); } public long getValue() { return holder.value; } public int getState() { return holder.state; } private static class ResultHolder { public final int state; public final long value; ResultHolder(int state, long value) { this.state = state; this.value = value; } } }
内容的提问来源于stack exchange,提问作者alphashock

