Arquitectura de hilos en Java y sincronización: del CPU a la JVM
El código Java se compila en bytecode, que la JVM convierte en instrucciones de máquina para el procesador. Cada núcleo de CPU ejecuta instrucciones de forma secuencial: una operación simple como un desplazamiento de bits en un registro ocurre en un ciclo de reloj. Los registros son celdas de memoria ultrarrápidas para almacenar números, direcciones y datos.
Los procesadores modernos usan arquitectura superscalar. Hyper-Threading permite que un solo hilo de hardware ejecute 2–6 instrucciones por ciclo, creando la ilusión de múltiples hilos lógicos por núcleo mediante cambios rápidos de contexto.
El número de hilos que se ejecutan simultáneamente está limitado por el número de núcleos. Los hilos comparten la memoria heap para objetos, mientras que cada uno tiene su propio stack para variables locales y referencias a objetos.
Condiciones de carrera y estructura de memoria
Una condición de carrera ocurre cuando varios hilos leen y modifican el mismo objeto en el heap al mismo tiempo. El resultado es impredecible, dependiendo del orden de las operaciones de lectura-modificación-escritura.
El código suele funcionar bien en local sin carga, pero en producción con multihilo, surgen colisiones de forma irregular. Un proceso del SO asigna memoria compartida; los hilos dentro de él son unidades de ejecución ligeras que compiten por recursos.
Mecanismos de sincronización: wait/notify y monitores
Cada objeto Java tiene un monitor (bloqueo) integrado. El bloque synchronized adquiere el monitor del objeto, garantizando:
- Exclusión mutua: Solo un hilo ejecuta el código dentro del bloque.
- Visibilidad: Los cambios son visibles para otros hilos al salir del bloque (con soporte de
volatile).
El monitor incluye:
- Conjunto de entrada: Cola de hilos esperando para adquirirlo.
- Conjunto de espera: Hilos en estado de espera.
wait(), notify() y notifyAll() solo funcionan dentro de un bloque synchronized en el mismo objeto; de lo contrario, se lanza IllegalMonitorStateException.
wait(): Mueve el hilo al conjunto de espera.notify(): Despierta un hilo aleatorio (riesgo de despertar perdido).notifyAll(): Despierta todos los hilos (enfoque preferido).
Los métodos synchronized estáticos bloquean el monitor de la clase (Class), sin interferir con métodos de instancia (this).
Bloqueos avanzados: ReentrantLock y Semaphore
Para mayor control, usa java.util.concurrent.locks.ReentrantLock:
Lock lock = new ReentrantLock();
lock.lock();
try {
// sección crítica
} finally {
lock.unlock();
}
Métodos clave:
tryLock(): Adquiere sin esperar (devuelve false si falla).tryLock(timeout, unit): Espera con tiempo límite.lockInterruptibly(): Adquisición interrumpible.new ReentrantLock(true): Bloqueo justo (orden FIFO, más lento).
Semaphore limita el acceso:
Semaphore sem = new Semaphore(3); // 3 permisos
sem.acquire(); // adquirir
sem.release(); // liberar
Problemas en multihilo: interbloqueo y vivihilo
Interbloqueo: Dependencias cíclicas de bloqueos. El hilo A tiene lock1 y espera lock2; el hilo B hace lo contrario. El sistema se congela.
Evítalo con:
- Bloqueo en orden fijo.
- Uso de tiempos límite.
- Minimización de bloqueos anidados.
Vivihilo: Los hilos están ocupados pero no avanzan. Ejemplo: dos hilos ceden recursos cortésmente en un bucle, disparando la CPU sin progreso.
Síncrono vs asíncrono en hilos
Los métodos síncronos bloquean el hilo en E/S (p. ej., consulta a BD): el hilo espera inactivo la respuesta.
Asíncrono usa CompletableFuture, un contenedor para resultados diferidos (pendiente, completado o fallido).
Cadenas de operaciones:
thenApply(): Transforma el resultado.thenCompose(): Compone con otro Future.thenAccept(): Consume sin retorno.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "result")
.thenApply(s -> s.toUpperCase())
.thenAccept(System.out::println);
Esto libera el hilo para otras tareas, aumentando el rendimiento en entornos multihilo.
Lecciones clave
- Cada núcleo ejecuta un hilo de hardware; Hyper-Threading simula varios.
- Las condiciones de carrera surgen del heap compartido; la sincronización garantiza exclusión mutua y visibilidad.
- Prefiere
notifyAll()sobrenotify()para evitar despertares perdidos. - ReentrantLock amplía
synchronizedcon tiempos límite, interrupciones y equidad. - CompletableFuture permite pipelines asíncronos sin bloquear hilos.
— Editorial Team
Aún no hay comentarios.