Смущает тест jcstress при сбое ReentrantReadWriteLock#tryLock
Я пытаюсь справиться с 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
, Я полагаю, что, возможно, я делаю какую-то глупую ошибку в моих тестах jcstress в отношении JMM и чтения состояния блокировок.
К сожалению, я не могу определить проблему. Может ли кто-нибудь помочь мне понять (глупую?) Ошибку, которую я совершил?
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. Извиняюсь за кросс-постинг, но мне нужна помощь в этом, и поэтому я делаю репост в Stackru в надежде привлечь внимание широкой аудитории.
1 ответ
Ваши тесты проходят при запуске против jcstress 0.3. В версии 0.4 поведение изменено и теперь включает результаты проверок работоспособности, запускаемых при запуске (см. Этот коммит против ошибки, jcstress пропускает образцы, собранные во время проверок работоспособности).
Некоторые проверки работоспособности выполняются в одном потоке, и ваш тест не обрабатывает случай, когда оба актера вызываются одним и тем же потоком; вы тестируете блокировку повторного входа, поэтому блокировка чтения пройдет, если блокировка записи уже удерживается.
Это возможно ошибка в jcstress, так как документация по @Actor
говорит, что инварианты:
- Каждый метод вызывается только одним конкретным потоком.
- Каждый метод вызывается ровно один раз за
State
пример.
Хотя документация не так четко сформулирована, сгенерированный источник ясно дает понять, что намерение состоит в том, чтобы запустить каждого участника в его собственном потоке.
Один из способов обойти это - пропустить однопоточный корпус:
@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();
}
}
Как вы отметили в комментариях, финал @Outcome
в приведенном выше тесте никогда не бывает. Это потому, что однопоточная проверка работоспособности не тасует актеров перед их запуском (см. Метод sanityCheck_Footprints
на ваш сгенерированный тестовый класс).