对 ReentrantReadWriteLock#tryLock 失败的 jcstress 测试感到困惑

我正试图掌握JCStress。为了确保我理解它,我决定为我知道必须正确的东西写一些简单的测试:.java.util.concurrent.locks.ReentrantReadWriteLock

我写了一些非常简单的测试来检查锁定模式兼容性。不幸的是,其中两个压力测试失败了:

  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
    

在我看来,一个线程不应该能够持有读锁定,而另一个线程也持有写锁定。同样,两个线程也不可能同时持有写锁定。

我意识到问题可能不在于 .我想我可能在jcstress测试中犯了一些关于JMM和读取锁状态的愚蠢错误。ReentrantReadWriteLock

不幸的是,我无法发现问题。有人可以帮我理解我所犯的(愚蠢的?)错误吗?

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

我确实尝试过询问此事,但从未收到回复 - http://mail.openjdk.java.net/pipermail/jcstress-dev/2018-August/000346.html。很抱歉交叉发布,但我需要帮助,所以我正在重新发布到StackOverflow,希望得到更多观众的关注。jcstress-dev


答案 1

测试在针对 jcstress 0.3 运行时通过。在版本 0.4 中,行为已更改为包括在启动时运行的健全性检查的结果(请参阅针对 bug jcstress 省略在健全性检查期间收集的样本的此提交)。

一些健全性检查在单个线程中运行,并且您的测试不处理两个参与者由同一线程调用的情况;您正在测试重入锁,因此如果已持有写锁定,则读锁定将通过。

这可以说是jcstress中的一个错误,因为文档说不变量是:@Actor

  • 每个方法仅由一个特定线程调用。
  • 每个方法每个实例只调用一次。State

虽然文档的措辞不那么清晰,但生成的源代码清楚地表明,其目的是在自己的线程中运行每个Actor。

解决此问题的一种方法是允许单线程案例通过:

@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();
    }
}

或者检查单线程案例并将其标记为“有趣”而不是接受:

@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();
    }
}

正如您在评论中指出的那样,上述测试中的最终情况永远不会发生。这是因为单线程健全性检查在运行 actor 之前不会随机排列它们(请参阅生成的测试类上的方法)。@OutcomesanityCheck_Footprints


答案 2

推荐