variable threads thread lock java multithreading concurrency java.util.concurrent jcstress

threads - reentrantlock java 8



Confundido por la prueba de jcstress en ReentrantReadWriteLock#tryLock falla (1)

Estoy tratando de familiarizarme con JCStress. Para asegurarme de que lo entiendo, decidí escribir algunas pruebas simples para algo que sé que debe ser correcto: java.util.concurrent.locks.ReentrantReadWriteLock .

Escribí algunas pruebas muy simples para verificar la compatibilidad del modo de bloqueo. Desafortunadamente dos de las pruebas de estrés están fallando:

  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

Me parece que un hilo no debería ser capaz de mantener el bloqueo de lectura, mientras que otro hilo también tiene el bloqueo de escritura. Del mismo modo, debería ser imposible para dos hilos mantener simultáneamente el bloqueo de escritura.

Me doy cuenta de que el problema probablemente no sea con ReentrantReadWriteLock . Me imagino que probablemente estoy cometiendo un error estúpido en mis pruebas de jcstress con respecto al JMM y leyendo el estado de los bloqueos.

Desafortunadamente, no soy capaz de detectar el problema. ¿Puede alguien ayudarme, por favor, a entender el error (estúpido) que he cometido?

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 preguntar sobre esto en el jcstress-dev pero nunca recibí una respuesta - http://mail.openjdk.java.net/pipermail/jcstress-dev/2018-August/000346.html . Disculpas por la publicación cruzada, pero necesito ayuda con esto, por lo que estoy reenviando a StackOverflow con la esperanza de obtener atención de un público más amplio.


Tus pruebas pasan cuando corres contra jcstress 0.3. En la versión 0.4, el comportamiento cambió para incluir los resultados de las comprobaciones de integridad que se ejecutan en el inicio (ver este compromiso contra el error jcstress omite las muestras recopiladas durante las comprobaciones de integridad ).

Algunas de las comprobaciones de validez se ejecutan en un solo hilo, y su prueba no controla el caso en el que ambos actores son llamados por el mismo hilo; está probando un bloqueo de reentrada , por lo que el bloqueo de lectura pasará si el bloqueo de escritura ya está retenido.

Podría decirse que esto es un error en jcstress, ya que la documentación en @Actor dice que los invariantes son:

  • Cada método es llamado solo por un hilo en particular.
  • Cada método se llama exactamente una vez por instancia de State .

Si bien la documentación no está claramente redactada, la fuente generada deja claro que la intención es ejecutar a cada actor en su propio hilo.

Una forma de solucionarlo sería permitir que pase el caso de un solo hilo:

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

O compruebe el caso de un solo hilo y márquelo como "interesante" en lugar de aceptado:

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

Como anotó en los comentarios, la @Outcome de @Outcome en la prueba anterior nunca ocurre. Esto se debe a que la comprobación de sanityCheck_Footprints de un solo hilo no baraja los actores antes de ejecutarlos (consulte el método sanityCheck_Footprints en su clase de prueba generada).