18

I am trying to get to grips with JCStress. To ensure I understand it, I decided to write some simple tests for something that I know must be correct: java.util.concurrent.locks.ReentrantReadWriteLock.

I wrote some very simple tests to check lock mode compatibility. Unfortunately two of the stress tests are failing:

  1. X_S:

    true, true        32,768     FORBIDDEN  No default case provided, assume FORBIDDEN
    
  2. X_X:

    true, true        32,767     FORBIDDEN  No default case provided, assume FORBIDDEN
    

It seems to me that one thread should not be able to hold the read lock, whilst another thread also holds the write lock. Likewise, it should be impossible for two threads to simultaneously hold the write lock.

I realise that the problem is likely not with ReentrantReadWriteLock. I figure that I am probably making some stupid mistake in my jcstress tests with regards to the JMM and reading the state of the locks.

Unfortunately, I am not able to spot the problem. Can someone please help me understand the (stupid?) mistake that I have made?

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.ZZ_Result;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/*
 * |-----------------|
 * |  COMPATIBILITY  |
 * |-----------------|
 * |     | S   | X   |
 * |-----------------|
 * | S   | YES | NO  |
 * | X   | NO  | NO  |
 * |-----------------|
 */
public class ReentrantReadWriteLockBooleanCompatibilityTest {

    @State
    public static class S {
        public final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

        public boolean shared() {
            return lock.readLock().tryLock();
        }

        public boolean exclusive() {
            return lock.writeLock().tryLock();
        }
    }

    @JCStressTest
    @Outcome(id = "true, true", expect = Expect.ACCEPTABLE, desc = "T1 and T2 are both acquired S")
    public static class S_S {
        @Actor
        public void actor1(S s, ZZ_Result r) { r.r1 = s.shared(); }
        @Actor
        public void actor2(S s, ZZ_Result r) { r.r2 = s.shared(); }
    }

    @JCStressTest
    @Outcome(id = "true, false", expect = Expect.ACCEPTABLE, desc = "T1 acquired S, and T2 could not acquire X")
    @Outcome(id = "false, true", expect = Expect.ACCEPTABLE, desc = "T2 acquired X, and T1 could not acquire S")
    public static class S_X {
        @Actor
        public void actor1(S s, ZZ_Result r) { r.r1 = s.shared(); }
        @Actor
        public void actor2(S s, ZZ_Result r) { r.r2 = s.exclusive(); }
    }

    @JCStressTest
    @Outcome(id = "true, false", expect = Expect.ACCEPTABLE, desc = "T1 acquired X, and T2 could not acquire S")
    @Outcome(id = "false, true", expect = Expect.ACCEPTABLE, desc = "T2 acquired S and T1 could not acquire X")
    public static class X_S {
        @Actor
        public void actor1(S s, ZZ_Result r) { r.r1 = s.exclusive(); }
        @Actor
        public void actor2(S s, ZZ_Result r) { r.r2 = s.shared(); }
    }

    @JCStressTest
    @Outcome(id = "true, false", expect = Expect.ACCEPTABLE, desc = "T1 acquired X, and T2 could not acquire X")
    @Outcome(id = "false, true", expect = Expect.ACCEPTABLE, desc = "T2 acquired X and T1 could not acquire X")
    public static class X_X {
        @Actor
        public void actor1(S s, ZZ_Result r) { r.r1 = s.exclusive(); }
        @Actor
        public void actor2(S s, ZZ_Result r) { r.r2 = s.exclusive(); }
    }
}

I did try asking about this on the jcstress-dev but never received a response - http://mail.openjdk.java.net/pipermail/jcstress-dev/2018-August/000346.html. Apologies for cross-posting, but I need help with this, and so I am reposting to StackOverflow in the hope of getting attention from a larger audience.

adamretter
  • 3,885
  • 2
  • 23
  • 43

1 Answers1

5

Your tests pass when run against jcstress 0.3. In version 0.4 the behaviour changed to include the results of the sanity checks that are run on startup (see this commit against the bug jcstress omits samples gathered during sanity checks).

Some of the sanity checks run in a single thread, and your test doesn't handle the case where both actors are called by the same thread; you're testing a reentrant lock, so the read lock will pass if the write lock is already held.

This is arguably a bug in jcstress, as the documentation on @Actor says the invariants are:

  • Each method is called only by one particular thread.
  • Each method is called exactly once per State instance.

While the documentation is not that clearly worded, the generated source makes it clear that the intention is to run each actor in its own thread.

One way to work around it would be to allow the single threaded case to pass:

@State
public static class S {
    public final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public boolean shared() {
        return lock.readLock().tryLock();
    }

    public boolean exclusive() {
        return lock.writeLock().tryLock();
    }

    public boolean locked() {
        return lock.isWriteLockedByCurrentThread();
    }
}

@JCStressTest
@Outcome(id = "true, false, false", expect = Expect.ACCEPTABLE, desc = "T1 acquired X, and T2 could not acquire S")
@Outcome(id = "false, false, true", expect = Expect.ACCEPTABLE, desc = "T2 acquired S and T1 could not acquire X")
@Outcome(id = "true, true, true", expect = Expect.ACCEPTABLE, desc = "T1 acquired X and then acquired S")
public static class X_S {
    @Actor
    public void actor1(S s, ZZZ_Result r) {
        r.r1 = s.exclusive();
    }
    @Actor
    public void actor2(S s, ZZZ_Result r) {
        r.r2 = s.locked();
        r.r3 = s.shared();
    }
}

Or check for the single threaded case and mark it as "interesting" instead of accepted:

@State
public static class S {
    public final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public AtomicReference<Thread> firstThread = new AtomicReference<>();

    public boolean shared() {
        firstThread.compareAndSet(null, Thread.currentThread());
        return lock.readLock().tryLock();
    }

    public boolean exclusive() {
        firstThread.compareAndSet(null, Thread.currentThread());
        return lock.writeLock().tryLock();
    }

    public boolean sameThread() {
        return Thread.currentThread().equals(firstThread.get());
    }

    public boolean locked() {
        return lock.isWriteLockedByCurrentThread();
    }
}

@JCStressTest
@Outcome(id = "false, true, false, false", expect = Expect.ACCEPTABLE, desc = "T1 acquired X, and T2 could not acquire X")
@Outcome(id = "false, false, false, true", expect = Expect.ACCEPTABLE, desc = "T2 acquired X and T1 could not acquire X")
@Outcome(id = "false, true, true, true", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Both actors ran in the same thread!")
@Outcome(id = "true, true, false, true", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Both actors ran in the same thread!")
public static class X_X {
    @Actor
    public void actor1(S s, ZZZZ_Result r) {
        r.r1 = s.sameThread();
        r.r2 = s.exclusive();
    }
    @Actor
    public void actor2(S s, ZZZZ_Result r) {
        r.r3 = s.sameThread();
        r.r4 = s.exclusive();
    }
}

As you noted in the comments, the final @Outcome in the above test never happens. This is because the single threaded sanity check doesn't shuffle the actors before running them (see the method sanityCheck_Footprints on your generated test class).

teppic
  • 7,051
  • 1
  • 29
  • 35
  • Hmm, this is very interesting! I had made the assumption that each Actor was a different thread. I didn't find anywhere in the documentation that stated otherwise. Actually I can't find anywhere which describes the mapping of Actors to Threads. Do you know of any official documentation or authority for this? I just had a look at the code for jcstress-core itself. It seems it uses a Cached Thread Pool - `Executors.newCachedThreadPool` of n threads where n is `Math.min(actorsCount, hardwareThreadsCount)`. So from the code I guess that a single thread could be reused for both actors... – adamretter Oct 01 '18 at 06:33
  • The only documentation I know of is on `@Actor`. I've just re-read it a couple of times, and it's not exactly clear, is it? It does, however, say _two or more Actor methods may be used to model the concurrent execution on data held by State instance_. – teppic Oct 01 '18 at 07:01
  • Yeah I did read the JavaDoc on `@Actor` previously. So I guess I am wondering... Apart from my strange test results, how did *you* know that two actors could run on the same thread? – adamretter Oct 01 '18 at 07:21
  • Frankly, I felt _less_ sure when I looked up the doc on `@Actor` after your comment an hour ago. I had to read through the `Runner` source to reassure myself. – teppic Oct 01 '18 at 07:37
  • Okay thanks :-) Give me a day or so to test out your suggestions, and then I can accept the answer. Thanks for all your help so far :-) – adamretter Oct 01 '18 at 07:46
  • So for the JCStressTest X_X I followed your `ZZZZ_Result` suggestion. I think the following four cases should be ACCEPTABLE - 1. "false, true, false, false", 2. "false, false, false, true", 3. "false, true, true, true", 4. "true, true, false, true" However (case 4) never occurs even under high-iterations. I assume that this is just a thread-scheduling fluke? – adamretter Oct 02 '18 at 10:01
  • I get the same result. Something is not quite right here. Also, I've noticed that the passing result for the single threaded tests is always _32767_, which is a very suspicious number. – teppic Oct 02 '18 at 19:01
  • It's happening because `sanityCheck_Footprints` (generated code for each test) always runs the actors in the same order, and always in a single thread. – teppic Oct 02 '18 at 19:52
  • Looking at the generated source again, it's clear that the intention is to run with one thread per actor. Each generated actor calls `StateHolder.preRun` before delegating to the actor implementation, which waits for the other actors to become ready in parallel. I don't think that the sanity check results should be included in the report because they violate this contract (and they weren't included in v0.3). I've rewritten the answer to reflect this. – teppic Oct 02 '18 at 22:08
  • Thanks very much @teppic. I am going to accept this answer and add a few bounty points. Just wondering how to report the possible bug to the jcstress authors, their mailing list seems a bit unresponsive. – adamretter Oct 07 '18 at 06:03