Confused by jcstress test на ReentrantReadWriteLock # tryLock сбой

Я пытаюсь справиться с 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
    

Мне кажется, что один поток не может удерживать блокировку чтения, в то время как другой поток также содержит блокировку записи. Аналогичным образом, для двух потоков одновременное удерживание блокировки записи должно быть невозможным.

Я понимаю, что проблема, скорее всего, не с 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. Извинения за перекрестную рассылку, но мне нужна помощь в этом, и поэтому я переписываю StackOverflow в надежде привлечь внимание более широкой аудитории.

Ответ 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 не перетасовывает участников перед их запуском (см. Метод sanityCheck_Footprints на сгенерированном тестовом классе).