Historia de la pregunta
Antes de Java 5, la coordinación de hilos dependía de métodos primitivos como Thread.suspend (deprecado debido a riesgos inherentes de interbloqueo) o Object.wait/notify, que requerían una estricta propiedad del monitor y sufrían de pérdidas de despertadores si la notificación ocurría antes de la espera. Con la introducción de java.util.concurrent en Java 5 (JSR 166), LockSupport fue diseñado como un primitivo de bloqueo de bajo nivel para permitir la construcción de sincronizadores de alto rendimiento como AbstractQueuedSynchronizer, sin la carga de los bloques intrínsecos.
El problema
En la programación concurrente, una condición de carrera clásica ocurre cuando un hilo de señalización invoca el mecanismo de desaparcamiento antes de que el hilo objetivo realmente aparque. Con las variables de condición tradicionales, esta señal se perdería, causando que el hilo objetivo durmiera indefinidamente. Una solución ingenua podría utilizar un semáforo contador para acumular permisos, pero esto introduce complejidad innecesaria y posibles fugas de recursos si el productor supera al consumidor.
La solución
LockSupport emplea un permiso de un solo bit y no acumulativo asociado con cada hilo. Este permiso actúa como un pase de puerta desechable y local al hilo:
Dado que el permiso no es acumulativo (se satura en 1), previene fugas de memoria por desaparcamiento excesivo, garantizando que un unpark emitido antes del park será recordado, eliminando así el problema de pérdida de despertadores a través de una relación de sucede antes.
import java.util.concurrent.locks.LockSupport; public class PermitExample { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { System.out.println("Trabajador: Trabajo inicial..."); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("Trabajador: Intentando aparcar..."); LockSupport.park(); System.out.println("Trabajador: ¡Desaparcado exitosamente!"); }); worker.start(); // Señal antes de que el trabajador realmente aparque Thread.sleep(50); System.out.println("Principal: Llamando unpark antes de que el trabajador aparque"); LockSupport.unpark(worker); worker.join(); } }
Descripción del problema
Mientras diseñábamos el motor de coincidencia de pedidos de un sistema de comercio de alta frecuencia, necesitábamos un mecanismo de retroalimentación donde los hilos consumidores pudieran suspender el procesamiento cuando la cola de entrada alcanzara la capacidad, sin mantener bloqueos que impidieran a los productores verificar el estado de la cola. El ReentrantLock estándar con Condition creó contención sobre el bloqueo de la cola durante la señalización, y Object.wait/notify sufrió el riesgo de pérdida de despertadores durante carreras de alta rotación.
Diferentes soluciones consideradas
1. Object.wait/notifyAll
Este enfoque utilizó el bloqueo intrínseco de la cola. Pros: Sencillo de implementar utilizando monitores estándar. Contras: Requería que el productor adquiriera el monitor para llamar a notify, creando un cuellos de botella de serialización. Peor aún, si un productor llamaba a notify durante la breve ventana entre que el consumidor verificaba el tamaño de la cola y llamaba a wait, la señal se perdía, causando un bloqueo permanente del consumidor.
2. ReentrantLock con múltiples Conditions
Intentamos usar condiciones separadas para los estados "lleno" y "vacío". Pros: Más flexible que los bloqueos intrínsecos, permitiendo despertadores selectivos. Contras: Aún requería adquisición de bloqueo para señalizar (signalAll), y la complejidad de transferir correctamente los hilos entre colas de condición introducía una sobrecarga de mantenimiento sin resolver la sobrecarga de bloqueo fundamental.
3. LockSupport con estado atómico explícito
La solución elegida utilizó un AtomicBoolean para representar "permiso para proceder" y LockSupport para bloquear. Cuando la cola se llenó, el consumidor estableció atómicamente una bandera "needsParking" y luego aparcó. Los productores, después de eliminar un ítem, verificaron la bandera y llamaron unpark si estaba establecida. Pros: La señalización requería sin bloqueos, eliminando la contención durante los despertadores. El modelo de permiso de un bit aseguraba que incluso si el productor llamaba unpark nanosegundos antes de que el consumidor llamara park (debido a la programación de la CPU), el despertador no se perdía.
Solución elegida y resultado
Seleccionamos el enfoque de LockSupport. Al desacoplar el mecanismo de señalización del bloqueo estructural de la cola, redujimos la latencia del productor en un 40% bajo carga pesada y eliminamos los escenarios de pérdida de despertadores observados durante las pruebas de estrés. La gestión de estado explícito (verificación doble de la condición después de unpark) garantizó la corrección a pesar del contrato de despertadores espurios de park().
¿LockSupport.park libera la propiedad de los monitores mantenidos por el hilo?
No. Esta es una distinción crítica de Object.wait(). Cuando un hilo invoca LockSupport.park, entra en un estado de espera pero retiene la propiedad de todos los monitores que actualmente posee. Si otro hilo intenta entrar en uno de esos monitores (por ejemplo, un bloque sincronizado en el mismo objeto), se bloqueará, potencialmente causando un interbloqueo si el hilo aparcado es el único que podría liberarlo. Los candidatos suelen asumir erróneamente que park es como wait y libera bloqueos; es un primitivo de programación de planificación puramente local al hilo.
¿Cuál es el comportamiento de LockSupport.park cuando se invoca en un hilo cuyo estado de interrupción está establecido?
El método regresa inmediatamente sin bloquear y no borra el estado de interrupción. Esto difiere fundamentalmente de Object.wait(), que borra el estado de interrupción y lanza InterruptedException. Con LockSupport, el hilo debe verificar y borrar explícitamente el estado de interrupción (a través de Thread.interrupted()) si desea respetar las convenciones de interrupción. Este diseño permite que park se utilice en contextos no interrumpibles o donde la interrupción se maneje como una preocupación separada del permiso de aparcamiento.
¿Cómo maneja LockSupport los despertadores espurios, y cómo afecta esto a los patrones de codificación?
LockSupport.park está documentado para regresar "sin razón" (despertador espurio), aunque en la práctica, esto es raro en JVM modernas. A diferencia del despertador basado en permisos (unpark), los despertadores espurios no consumen el permiso. Por lo tanto, el llamador debe siempre volver a verificar la condición que causó el aparcamiento en un bucle:
while (!canProceed()) { LockSupport.park(); }
Los candidatos a menudo pasan por alto que simplemente verificar la condición una vez después de park no es suficiente; el hilo podría despertarse espúreo (o debido a una interrupción accidental) sin una llamada a unpark, requiriendo una reevaluación de la condición de estado. El permiso asegura que un unpark válido no se pierda, pero no previene los retornos espurios.