Can somebody help me understand why I'm getting FORBIDDEN results in this JCStress? The scenario is simple.
We have one actor that records a result of some operation then changes the state (which is volatile). We also have two actors that are exactly the same - they record the state and the result of the operation specifically in that order. The test doesn't give any forbidden results with single reading actor.
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;
}
}
}
Here is one of the outputs.
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
The second actor 'getStateAndResult2' is the cause. If you remove it, it will run without problems.
The cause of the unexpected behavior is that the same result instance is shared between the getStateAndResult1 and getStateAndResult2 actors which causes a race condition.
Check this version that is part of JCstress for causality testing:
I made a slightly modified version that demonstrates that the instance is shared:
package com.example.jcstress;
import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;
import org.openjdk.jcstress.infra.results.IJ_Result;
import java.util.concurrent.atomic.AtomicReference;
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 volatile int state;
private long value;
private volatile IJ_Result result1;
private volatile IJ_Result result2;
public SingleWriterMultiReaderTest() {
this.value = 0L;
this.state = 0;
}
@Actor
public void actor1() {
this.value = 12345L;
this.state = 1;
}
@Actor
public void actor2(IJ_Result result) {
result1 = result;
if(result2 == result){
throw new RuntimeException();
}
result.r1 = state;
result.r2 = value;
}
@Actor
public void actor3(IJ_Result result) {
result2 = result;
if(result1 == result){
throw new RuntimeException();
}
result.r1 = state;
result.r2 = value;
}
}
}