¿Por qué usar un ReentrantLock si uno puede usar sincronizado (esto)?

317

Estoy tratando de entender qué hace que el bloqueo en concurrencia sea tan importante si se puede usar synchronized (this). En el siguiente código ficticio, puedo hacer lo siguiente:

  1. sincronizó todo el método o sincronizó el área vulnerable ( synchronized(this){...})
  2. O bloquee el área de código vulnerable con un ReentrantLock.

Código:

    private final ReentrantLock lock = new ReentrantLock(); 
    private static List<Integer> ints;

    public Integer getResult(String name) { 
        .
        .
        .
        lock.lock();
        try {
            if (ints.size()==3) {
                ints=null;
                return -9;
            }   

            for (int x=0; x<ints.size(); x++) {
                System.out.println("["+name+"] "+x+"/"+ints.size()+". values >>>>"+ints.get(x));
            }

        } finally {
            lock.unlock();
        } 
        return random;
}
adhg
fuente
1
Por cierto, todas las cerraduras intrínsecas de Java son reentrantes por naturaleza.
Aniket Thakur
@pongapundit por synchronized(this){synchronized(this){//some code}}lo que no causará un bloqueo muerto. Para el bloqueo intrínseco si obtienen un monitor en un recurso y si lo desean de nuevo, pueden obtenerlo sin un bloqueo inactivo.
Aniket Thakur

Respuestas:

474

Un ReentrantLock no está estructurado , a diferencia de las synchronizedconstrucciones, es decir, no necesita usar una estructura de bloque para bloquear e incluso puede mantener un bloqueo en todos los métodos. Un ejemplo:

private ReentrantLock lock;

public void foo() {
  ...
  lock.lock();
  ...
}

public void bar() {
  ...
  lock.unlock();
  ...
}

Tal flujo es imposible de representar a través de un solo monitor en una synchronizedconstrucción.


Aparte de eso, ReentrantLockadmite sondeo de bloqueo y esperas de bloqueo interrumpible que admiten tiempo de espera . ReentrantLocktambién tiene soporte para una política de equidad configurable , lo que permite una programación de subprocesos más flexible.

El constructor de esta clase acepta un parámetro opcional de equidad . Cuando se establece true, bajo contención, los bloqueos favorecen el acceso al hilo que espera más tiempo. De lo contrario, este bloqueo no garantiza ninguna orden de acceso en particular. Los programas que usan bloqueos justos a los que acceden muchos subprocesos pueden mostrar un rendimiento general más bajo (es decir, son más lentos; a menudo mucho más lentos) que los que usan la configuración predeterminada, pero tienen variaciones más pequeñas en ocasiones para obtener bloqueos y garantizar la falta de inanición. Sin embargo, tenga en cuenta que la imparcialidad de las cerraduras no garantiza la imparcialidad de la programación de hilos. Por lo tanto, uno de los muchos subprocesos que utilizan un bloqueo equitativo puede obtenerlo varias veces seguidas, mientras que otros subprocesos activos no progresan y actualmente no mantienen el bloqueo. También tenga en cuenta que el tiempo notryLockEl método no respeta el ajuste de equidad. Tendrá éxito si el bloqueo está disponible, incluso si otros hilos están esperando.


ReentrantLock También puede ser más escalable , con un rendimiento mucho mejor bajo una mayor contención. Puedes leer más sobre esto aquí .

Sin embargo, esta afirmación ha sido impugnada; ver el siguiente comentario:

En la prueba de bloqueo reentrante, se crea un nuevo bloqueo cada vez, por lo que no hay bloqueo exclusivo y los datos resultantes no son válidos. Además, el enlace de IBM no ofrece ningún código fuente para el punto de referencia subyacente, por lo que es imposible caracterizar si la prueba incluso se realizó correctamente.


¿Cuándo deberías usar ReentrantLocks? De acuerdo con ese artículo de developerWorks ...

La respuesta es bastante simple: úsela cuando realmente necesite algo que no proporciona synchronized, como esperas de bloqueo programadas, esperas de bloqueo interrumpibles, bloqueos sin estructura de bloque, variables de condición múltiple o sondeo de bloqueo. ReentrantLocktambién tiene beneficios de escalabilidad, y debe usarlo si realmente tiene una situación que exhibe una alta contención, pero recuerde que la gran mayoría de los synchronizedbloques casi nunca exhiben ninguna contención, y mucho menos una alta contención. Aconsejaría desarrollar con sincronización hasta que la sincronización haya demostrado ser inadecuada, en lugar de simplemente asumir que "el rendimiento será mejor" si usaReentrantLock. Recuerde, estas son herramientas avanzadas para usuarios avanzados. (Y los usuarios verdaderamente avanzados tienden a preferir las herramientas más simples que pueden encontrar hasta que estén convencidos de que las herramientas simples son inadecuadas). Como siempre, primero haga las cosas bien y luego se preocupe si tiene que hacerlo más rápido o no.

oldrinb
fuente
26
El enlace 'se sabe que es más escalable' a lycog.com debe eliminarse. En la prueba de bloqueo reentrante, se crea un nuevo bloqueo cada vez, por lo que no hay bloqueo exclusivo y los datos resultantes no son válidos. Además, el enlace de IBM no ofrece ningún código fuente para el punto de referencia subyacente, por lo que es imposible caracterizar si la prueba incluso se realizó correctamente. Personalmente, simplemente eliminaría toda la línea sobre la escalabilidad, ya que todo el reclamo es esencialmente incompatible.
Dev
2
Modifiqué la publicación a la luz de tu respuesta.
oldrinb
66
Si el rendimiento es una gran preocupación para usted, no olvide buscar una forma en la que NO necesite sincronización.
mcoolive
2
Lo del rendimiento no tiene sentido para mí en absoluto. Si el bloqueo reactiva funcionaría mejor, ¿por qué la sincronización no se implementaría de la misma manera que un bloqueo reactiva internamente?
tObi
2
@ user2761895 el ReentrantLockPseudoRandomcódigo en el enlace de Lycog está usando bloqueos nuevos e incontenidos cada invocación de setSeedynext
oldrinb
14

ReentrantReadWriteLockes una cerradura especializada, mientras que synchronized(this)es una cerradura de uso general. Son similares pero no exactamente iguales.

Tienes razón en que podrías usar en synchronized(this)lugar de, ReentrantReadWriteLockpero lo contrario no siempre es cierto.

Si desea comprender mejor lo que hace ReentrantReadWriteLockespecial, busque información sobre la sincronización de subprocesos productor-consumidor.

En general, puede recordar que la sincronización de todo el método y la sincronización de propósito general (usando la synchronizedpalabra clave) se pueden usar en la mayoría de las aplicaciones sin pensar demasiado en la semántica de la sincronización, pero si necesita exprimir el rendimiento de su código, es posible que necesite explore otros mecanismos de sincronización más específicos o más específicos.

Por cierto, usar synchronized(this), y en general bloquear usando una instancia de clase pública, puede ser problemático porque abre su código a posibles bloqueos muertos porque alguien más, a sabiendas, podría intentar bloquear su objeto en otro lugar del programa.

Mike Dinescu
fuente
para evitar posibles bloqueos muertos porque alguien más, a sabiendas, podría intentar bloquear su objeto en otro lugar del programa, use una instancia de objeto privada como monitor de sincronización como este: public class MyLock { private final Object protectedLongLockingMonitor = new Object(); private long protectedLong = 0L; public void incrementProtectedLong() { synchronized(protectedLongLockingMonitor) { protectedLong++; } } }
sushicutta
9

Desde la página de documentación de Oracle sobre ReentrantLock :

Un bloqueo de exclusión mutua reentrante con el mismo comportamiento básico y semántica que el bloqueo de monitor implícito al que se accede mediante métodos y declaraciones sincronizados, pero con capacidades extendidas.

  1. Un ReentrantLock es propiedad del último hilo de bloqueo con éxito, pero aún no desbloquearlo. Volverá un bloqueo de invocación de subproceso, adquiriendo el bloqueo con éxito, cuando el bloqueo no es propiedad de otro subproceso. El método volverá inmediatamente si el hilo actual ya posee el bloqueo.

  2. El constructor de esta clase acepta una equidad opcional. parámetro . Cuando se establece como verdadero, bajo contención, los bloqueos favorecen la concesión de acceso al hilo que más espera . De lo contrario, este bloqueo no garantiza ninguna orden de acceso en particular.

ReentrantLock clave de según este artículo

  1. Capacidad para bloquear de forma interrumpida.
  2. Capacidad de tiempo de espera mientras se espera el bloqueo.
  3. Poder para crear un bloqueo justo.
  4. API para obtener la lista de subprocesos en espera de bloqueo.
  5. Flexibilidad para intentar bloquear sin bloquear.

Puede usar ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock para obtener más control sobre el bloqueo granular en las operaciones de lectura y escritura.

Eche un vistazo a este artículo de Benjamen sobre el uso de diferentes tipos de ReentrantLocks

Ravindra babu
fuente
2

Puede usar bloqueos reentrantes con una política de equidad o tiempo de espera para evitar el hambre de hilos. Puede aplicar una política de equidad de hilo. ayudará a evitar que un hilo espere para siempre para llegar a sus recursos.

private final ReentrantLock lock = new ReentrantLock(true);
//the param true turns on the fairness policy. 

La "política de equidad" elige el siguiente hilo ejecutable para ejecutar. Se basa en la prioridad, el tiempo desde la última ejecución, bla, bla

Además, Sincronizar puede bloquear indefinidamente si no puede escapar del bloqueo. Reentrantlock puede tener un tiempo de espera establecido.

j2emanue
fuente
1

Los bloqueos sincronizados no ofrecen ningún mecanismo de cola de espera en el que, después de la ejecución de un hilo, cualquier hilo que se ejecute en paralelo pueda adquirir el bloqueo. Debido a que el subproceso que está allí en el sistema y que se ejecuta durante un período de tiempo más largo, nunca tiene la oportunidad de acceder al recurso compartido, lo que lleva al hambre.

Los bloqueos reentrantes son muy flexibles y tienen una política de equidad en la que si un hilo espera más tiempo y después de completar el hilo que se está ejecutando actualmente, podemos asegurarnos de que el hilo más largo en espera tenga la oportunidad de acceder al recurso compartido disminuyendo el rendimiento del sistema y hacerlo más lento.

Sumit Kapoor
fuente
1

Supongamos que este código se ejecuta en un hilo:

private static ReentrantLock lock = new ReentrantLock();

void accessResource() {
    lock.lock();
    if( checkSomeCondition() ) {
        accessResource();
    }
    lock.unlock();
}

Debido a que el subproceso posee el bloqueo, permitirá que múltiples llamadas se bloqueen (), por lo que volverá a ingresar al bloqueo. Esto se puede lograr con un recuento de referencia para que no tenga que volver a adquirir el bloqueo.

RonTLV
fuente
0

Una cosa a tener en cuenta es:

el nombre ' ReentrantLock ' emite un mensaje incorrecto sobre otro mecanismo de bloqueo que no son reentrantes. Esto no es verdad. El bloqueo adquirido a través de 'sincronizado' también es reentrante en Java.

La diferencia clave es que 'sincronizado' usa bloqueo intrínseco (uno que tiene cada Objeto) mientras que Lock API no.

Verano
fuente
0

Creo que los métodos wait / notify / notifyAll no pertenecen a la clase Object, ya que contamina todos los objetos con métodos que rara vez se usan. Tienen mucho más sentido en una clase de bloqueo dedicada. Entonces, desde este punto de vista, quizás sea mejor usar una herramienta que esté diseñada explícitamente para el trabajo en cuestión, es decir, ReentrantLock.

Solubris
fuente