Search code examples
javaconcurrencyjcstress

JCStress test results that I can't understand


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  

Solution

  • 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:

    https://github.com/openjdk/jcstress/blob/master/jcstress-samples/src/main/java/org/openjdk/jcstress/samples/jmm/basic/BasicJMM_06_Causality.java

    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;
            }
        }
    }