对 ReentrantReadWriteLock#tryLock 失败的 jcstress 测试感到困惑
Confused by jcstress test on ReentrantReadWriteLock#tryLock failing
我正在努力掌握 JCStress。为了确保我理解它,我决定为我知道必须正确的东西编写一些简单的测试:java.util.concurrent.locks.ReentrantReadWriteLock
.
我写了一些非常简单的测试来检查锁定模式的兼容性。不幸的是,有两项压力测试失败了:
X_S
:
true, true 32,768 FORBIDDEN No default case provided, assume FORBIDDEN
X_X
:
true, true 32,767 FORBIDDEN No default case provided, assume FORBIDDEN
在我看来,一个线程不能持有读锁,而另一个线程也持有写锁。同样,应该不可能有两个线程同时持有写锁。
我意识到问题可能与 ReentrantReadWriteLock
无关。我想我可能在关于 JMM 和读取锁状态的 jcstress 测试中犯了一些愚蠢的错误。
不幸的是,我无法发现问题所在。有人可以帮我理解我犯的(愚蠢的?)错误吗?
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(); }
}
}
我曾尝试在 jcstress-dev
上询问过这个问题,但从未收到回复 - http://mail.openjdk.java.net/pipermail/jcstress-dev/2018-August/000346.html。很抱歉交叉发布,但我需要帮助,所以我重新发布到 Whosebug,希望得到更多观众的关注。
当 运行 针对 jcstress 0.3 时,您的测试通过。在 0.4 版中,行为更改为包括启动时 运行 的健全性检查结果(参见 this commit against the bug jcstress omits samples gathered during sanity checks)。
一些健全性检查 运行 在单个线程中进行,而您的测试无法处理两个参与者都被同一线程调用的情况;您正在测试 可重入 锁,因此如果写锁已被持有,则读锁将通过。
这可以说是 jcstress 中的一个错误,因为 @Actor
上的文档说不变量是:
- Each method is called only by one particular thread.
- Each method is called exactly once per
State
instance.
虽然文档措辞不明确,但生成的源代码清楚地表明其目的是 运行 每个参与者都在自己的线程中。
解决它的一种方法是允许单线程案例通过:
@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();
}
}
或者检查单线程情况并将其标记为 "interesting" 而不是接受:
@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();
}
}
正如您在评论中指出的那样,上述测试中的最终 @Outcome
从未发生过。这是因为单线程健全性检查不会在 运行 之前对 actor 进行洗牌(请参阅生成的测试 class 上的方法 sanityCheck_Footprints
)。
我正在努力掌握 JCStress。为了确保我理解它,我决定为我知道必须正确的东西编写一些简单的测试:java.util.concurrent.locks.ReentrantReadWriteLock
.
我写了一些非常简单的测试来检查锁定模式的兼容性。不幸的是,有两项压力测试失败了:
X_S
:true, true 32,768 FORBIDDEN No default case provided, assume FORBIDDEN
X_X
:true, true 32,767 FORBIDDEN No default case provided, assume FORBIDDEN
在我看来,一个线程不能持有读锁,而另一个线程也持有写锁。同样,应该不可能有两个线程同时持有写锁。
我意识到问题可能与 ReentrantReadWriteLock
无关。我想我可能在关于 JMM 和读取锁状态的 jcstress 测试中犯了一些愚蠢的错误。
不幸的是,我无法发现问题所在。有人可以帮我理解我犯的(愚蠢的?)错误吗?
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(); }
}
}
我曾尝试在 jcstress-dev
上询问过这个问题,但从未收到回复 - http://mail.openjdk.java.net/pipermail/jcstress-dev/2018-August/000346.html。很抱歉交叉发布,但我需要帮助,所以我重新发布到 Whosebug,希望得到更多观众的关注。
当 运行 针对 jcstress 0.3 时,您的测试通过。在 0.4 版中,行为更改为包括启动时 运行 的健全性检查结果(参见 this commit against the bug jcstress omits samples gathered during sanity checks)。
一些健全性检查 运行 在单个线程中进行,而您的测试无法处理两个参与者都被同一线程调用的情况;您正在测试 可重入 锁,因此如果写锁已被持有,则读锁将通过。
这可以说是 jcstress 中的一个错误,因为 @Actor
上的文档说不变量是:
- Each method is called only by one particular thread.
- Each method is called exactly once per
State
instance.
虽然文档措辞不明确,但生成的源代码清楚地表明其目的是 运行 每个参与者都在自己的线程中。
解决它的一种方法是允许单线程案例通过:
@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();
}
}
或者检查单线程情况并将其标记为 "interesting" 而不是接受:
@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();
}
}
正如您在评论中指出的那样,上述测试中的最终 @Outcome
从未发生过。这是因为单线程健全性检查不会在 运行 之前对 actor 进行洗牌(请参阅生成的测试 class 上的方法 sanityCheck_Footprints
)。