Mecanismos Internos de ReentrantLock en Java

Proceso de Adquisición del Bloqueo

En el contexto de un bloqueo justo (Fair Lock), la rutina principle para adquirir el mutex se estructura de la siguiente manera. El método central delega la lógica en intentos de adquisición y gestión de cola:

public final void obtenerAcceso(int argumentos) {
    if (!intentarAdquirir(argumentos) &&
        gestionarCola(agregarEspera(Nodo.EXCLUSIVO), argumentos))
        interrumpirPropio();
}

Esta secuencia operativa se puede desglosar en tres fases fundamnetales.

1. Intento Inicial de Adquisición (tryAcquire)

El primer paso verifica si el estado del bloqueo permite la entrada inmediata. Se evalúa el estado actual y la propiedad del hilo:

protected final boolean intentarAdquirir(int unidades) {
    final Thread hiloActual = Thread.currentThread();
    int estadoActual = obtenerEstado();
    
    if (estadoActual == 0) {
        if (!existenPredecesoresEnCola() &&
            compararYEstablecerEstado(0, unidades)) {
            establecerHiloPropietario(hiloActual);
            return true;
        }
    }
    else if (hiloActual == obtenerHiloPropietario()) {
        int nuevoEstado = estadoActual + unidades;
        if (nuevoEstado < 0)
            throw new Error("Límite máximo de reentrancia excedido");
        establecerEstado(nuevoEstado);
        return true;
    }
    return false;
}

Si el estado es cero, indica que el bloqueo está libre. Sin embargo, en modo justo, se debe verificar existenPredecesoresEnCola() para asegurar que no haya hilos esperando antes que el actual, incluso si el bloqueo acaba de liberarse. La lógica de verificación es:

public final boolean existenPredecesoresEnCola() {
    Nodo t = cola; 
    Nodo h = cabecera;
    Nodo s;
    return h != t &&
        ((s = h.siguiente) == null || s.hilo != Thread.currentThread());
}

Si la cabecera y la cola coinciden, la cola está vacía. Si el primer nodo en espera es el hilo actual, tampoco hay predecesores. La validación (s = h.siguiente) == null maneja condiciones de carrera donde otro hilo podría estar insertándose simultáneamente mediante agregarEspera:

private Node agregarEspera(Node modo) {
    Node nodo = new Node(Thread.currentThread(), modo);
    Node predecesor = cola;
    if (predecesor != null) {
        nodo.anterior = predecesor;
        if (compararYEstablecerCola(predecesor, nodo)) {
            predecesor.siguiente = nodo;
            return nodo;
        }
    }
    inicializarCola(nodo);
    return nodo;
}

La inserción implica actualizar punteros de forma atómica. Si existenPredecesoresEnCola() retorna falso, se ejecuta una operación CAS sobre el estado. Si falla, se verifica la reentrancia: si el hilo propietario es el actual, se incrementa el estado. Dado que el estado es un entero, el límite máximo de reentrancia es 2^31-1.

2. Encolamiento ante Fallo (addWaiter)

Si la adquisición falla, el hilo se añade a la cola de sincronización. Si la cola ya está inicializada (predecesor != null), se intenta una ruta rápida. De lo contrario, se invoca la inicialización completa:

private Node inicializarCola(final Node nodo) {
    for (;;) {
        Node t = cola;
        if (t == null) { 
            if (compararYEstablecerCabecera(new Node()))
                cola = cabecera;
        } else {
            nodo.anterior = t;
            if (compararYEstablecerCola(t, nodo)) {
                t.siguiente = nodo;
                return t;
            }
        }
    }
}

El bucle infinito garantiza que la operación de encolamiento最终 se complete exitosamente.

3. Suspensión del Hilo (acquireQueued)

Una vez encolado, el hilo entra en un bucle de adquisición. Si su predecesor es la cabecera, intenta obtener el bloqueo nuevamente. Si falla, se suspende:

final boolean gestionarCola(final Node nodo, int arg) {
    boolean falloAdquisicion = true;
    try {
        boolean interrumpido = false;
        for (;;) {
            final Node p = nodo.predecesor();
            if (p == cabecera && intentarAdquirir(arg)) {
                establecerCabecera(nodo);
                p.siguiente = null; 
                falloAdquisicion = false;
                return interrumpido;
            }
            if (verificarSuspension(p, nodo) &&
                suspenderYVerificarInterrupcion())
                interrumpido = true;
        }
    } finally {
        if (falloAdquisicion)
            cancelarAdquisicion(nodo);
    }
}

Antes de suspenderse, se ejecuta verificarSuspension para gestionar el estado de espera del predecesor:

private static boolean verificarSuspension(Node pred, Node node) {
    int estadoEspera = pred.estadoEspera;
    if (estadoEspera == Nodo.SEÑAL)
        return true;
    if (estadoEspera > 0) {
        do {
            node.anterior = pred = pred.anterior;
        } while (pred.estadoEspera > 0);
        pred.siguiente = node;
    } else {
        compararYEstablecerEstadoEspera(pred, estadoEspera, Nodo.SEÑAL);
    }
    return false;
}

Este método asegura que el nodo predecesor tenga el estado SIGNAL (-1), indicando que debe despertar al sucesor al liberar el bloqueo. La primera iteración establece la señal; la sigueinte permite la suspensión real mediante llamadas nativas.

Principios de park/unpark

La suspensión se basa en primitivas nativas. Cada hilo posee un objeto Parker que mantiene un contador (_counter), un mutex (_mutex) y una variable de condición (_cond). park consume un permiso; unpark lo provee. Si el contador es cero, el hilo se bloquea; si es mayor que cero, se decrementa y continúa. Los permisos no son acumulativos.

Condition y Comparativa con synchronized

Mediante reentrantLock.newCondition(), se obtienen objetos Condition que gestionan colas de espera específicas para notificaciones condicionales.

Diferencias clave respecto a synchronized:

  • synchronized opera en métodos y bloques; ReentrantLock solo en bloques explícitos.
  • La gestión de locking en synchronized es automática (JVM), mientras que ReentrantLock requiere lock() y unlock() manuales.
  • synchronized es inherentemente no justo; ReentrantLock permite configurar justicia (fairness).
  • ReentrantLock soporta interrupción de hilos en espera; synchronized no responde a interrupt mientras espera el bloqueo.
  • La cola de espera en synchronized es una pila (wait/notify); ReentrantLock utiliza colas FIFO y múltiples colas de condición.
  • Solo ReentrantLock ofrece soporte nativo para múltiples condiciones de espera mediante la interfaz Condition.

Etiquetas: java ReentrantLock concurrencia AQS sincronización

Publicado el 8-27 10:28