En la lección anterior arrancaste hilos con start(), los esperaste con join() y los cancelaste con interrupt(). En el camino aparecieron dos valores de getState() —NEW y TERMINATED— sin explicar qué son ni cuántos más hay.
Esta lección los explica todos. Un hilo de Java está siempre en exactamente uno de seis estados, y saber en cuál está y por qué es la diferencia entre diagnosticar un problema de producción en cinco minutos o en dos días. Cuando una aplicación "se cuelga", la pregunta no es "¿por qué no funciona?" sino "¿en qué estado están sus hilos y qué están esperando?". Esa pregunta se responde con un volcado de hilos, y leerlo es una habilidad que vale por sí sola el precio de esta lección.
Verás también el mecanismo de coordinación de más bajo nivel que existe en Java —wait, notify y notifyAll—, que es lo que hay debajo de todas las utilidades cómodas de 08-05 y 08-06; por qué hay que envolverlo siempre en un bucle while y por qué un if ahí es un error real, no una preferencia de estilo; y por qué tres métodos de Thread que parecían muy útiles —stop, suspend y resume— llevan más de veinte años marcados como peligrosos.
Advertencia de nivel.
wait/notifyes la única parte de este módulo que probablemente nunca escribirás en producción:BlockingQueueyCountDownLatchlo hacen mejor y sin errores. Se estudia porque explica cómo funcionan esas herramientas y porque aparece constantemente al leer código ajeno y volcados de hilos.
Contenido
- Los seis estados de
Thread.State - El diagrama completo del ciclo de vida
NEWyTERMINATED: los extremosRUNNABLE: el matiz que confunde a todo el mundoBLOCKEDfrente aWAITING: la distinción que hace legible un volcadoTIMED_WAITING: esperar con plazowait,notifyynotifyAll- El bucle
whileobligatorio notifyfrente anotifyAllyieldyonSpinWait- Los métodos retirados:
stop,suspend,resume - Diagnóstico en la práctica: el volcado de hilos
- Leer un interbloqueo detectado por la JVM
- Inspección desde el propio programa
- BiblioTech: seguir el estado de sus hilos
- Errores Comunes y Consejos
- Ejercicios
- Los seis estados de
Thread.State
Thread.StateThread.State es un enum —de los de 04-07— con seis constantes. Un hilo está siempre en una y solo una de ellas.
| Estado | Significado | Cómo se llega | Cómo se sale |
|---|---|---|---|
NEW |
Objeto Thread creado, aún no arrancado |
new Thread(...) |
start() |
RUNNABLE |
Ejecutando, o listo para ejecutar y esperando núcleo | start(); volver de una espera |
Terminar run(); bloquearse; esperar |
BLOCKED |
Esperando a adquirir el monitor de un objeto | Entrar en synchronized ocupado |
El monitor queda libre y se adquiere |
WAITING |
Esperando indefinidamente una señal de otro hilo | wait(), join(), LockSupport.park() |
notify/notifyAll, fin del hilo esperado, interrupción |
TIMED_WAITING |
Igual que WAITING pero con plazo |
sleep(ms), wait(ms), join(ms), parkNanos |
Señal, plazo agotado o interrupción |
TERMINATED |
run() ha terminado (bien o con excepción) |
Retorno o excepción no capturada de run() |
Nunca: es terminal |
Dos hechos que conviene fijar desde el principio:
TERMINATEDes irreversible. Un hilo terminado no se puede reiniciar.start()sobre él lanzaIllegalThreadStateException. Esa es la razón profunda por la que existen los pools: si el hilo no puede reutilizarse, hay que reutilizar el hilo, no el objeto — es decir, mantenerlo vivo dándole tareas nuevas.Thread.Statees una vista de la JVM, no del sistema operativo. El estado que ves es lo que la JVM sabe; el planificador del sistema tiene su propia idea, más fina.
- El diagrama completo del ciclo de vida
stateDiagram-v2
[*] --> NEW : new Thread(tarea)
NEW --> RUNNABLE : start()
RUNNABLE --> BLOCKED : entra en synchronized ocupado
BLOCKED --> RUNNABLE : obtiene el monitor
RUNNABLE --> WAITING : wait()
RUNNABLE --> WAITING : join() sin plazo
RUNNABLE --> WAITING : LockSupport.park()
WAITING --> RUNNABLE : notify o notifyAll
WAITING --> RUNNABLE : termina el hilo esperado
WAITING --> RUNNABLE : interrupt()
RUNNABLE --> TIMED_WAITING : sleep(ms)
RUNNABLE --> TIMED_WAITING : wait(ms)
RUNNABLE --> TIMED_WAITING : join(ms)
TIMED_WAITING --> RUNNABLE : plazo agotado
TIMED_WAITING --> RUNNABLE : notify o interrupt
RUNNABLE --> TERMINATED : run() retorna
RUNNABLE --> TERMINATED : excepcion no capturada
TERMINATED --> [*]
Dos observaciones estructurales sobre este diagrama:
Primera: todo pasa por RUNNABLE. No hay ninguna transición directa entre BLOCKED, WAITING y TIMED_WAITING. Un hilo que está esperando una señal y luego necesita un monitor pasa por RUNNABLE en medio. Esto importa al leer volcados: un hilo que sale de wait() debe readquirir el monitor antes de continuar, y en ese instante puede quedar BLOCKED.
Segunda: solo hay una salida de TERMINATED, y es hacia fuera. No hay flecha de vuelta. Es el punto que hace que "reutilizar un hilo" sea imposible por diseño.
Este pequeño programa recorre casi todos los estados:
import java.util.concurrent.TimeUnit;
public class RecorridoDeEstados {
private static final Object candado = new Object();
public static void main(String[] args) throws InterruptedException {
Thread observado = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300); // TIMED_WAITING
synchronized (candado) { // BLOCKED (si esta ocupado)
candado.wait(500); // TIMED_WAITING
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "hilo-observado");
System.out.println("Antes de start() : " + observado.getState()); // NEW
observado.start();
System.out.println("Justo tras start() : " + observado.getState()); // RUNNABLE
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Durmiendo : " + observado.getState()); // TIMED_WAITING
// Tomamos el candado para que el hilo observado quede BLOCKED al pedirlo.
synchronized (candado) {
TimeUnit.MILLISECONDS.sleep(400);
System.out.println("Candado ocupado : " + observado.getState()); // BLOCKED
}
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Dentro de wait(500): " + observado.getState()); // TIMED_WAITING
observado.join();
System.out.println("Tras join() : " + observado.getState()); // TERMINATED
}
}Salida:
Antes de start() : NEW
Justo tras start() : RUNNABLE
Durmiendo : TIMED_WAITING
Candado ocupado : BLOCKED
Dentro de wait(500): TIMED_WAITING
Tras join() : TERMINATEDEste programa es intrínsecamente frágil y es honesto decirlo: depende de que los
sleepdel observador caigan en los momentos justos. En una máquina cargada podría imprimir otra cosa. Sirve para ver los estados, no como técnica de diagnóstico: para eso están los volcados del apartado 12.
NEW y TERMINATED: los extremos
NEW y TERMINATED: los extremosNEW es el estado del objeto Thread recién construido. El hilo del sistema operativo todavía no existe: new Thread(...) solo reserva un objeto Java. El hilo nativo se crea en start().
De aquí sale el diagnóstico del error de 08-02: si un hilo aparece como NEW cuando esperabas que estuviera trabajando, es que nadie llamó a start() —o que se llamó a run() por error—.
TERMINATED es el estado tras el fin de run(), por cualquiera de sus dos vías:
public class DosFormasDeTerminar {
public static void main(String[] args) throws InterruptedException {
Thread bien = new Thread(() -> System.out.println("[bien] trabajo hecho"), "bien");
Thread mal = new Thread(() -> { throw new RuntimeException("fallo"); }, "mal");
bien.start(); bien.join();
mal.start(); mal.join();
System.out.println("bien -> " + bien.getState()); // TERMINATED
System.out.println("mal -> " + mal.getState()); // TERMINATED
}
}Ambos acaban en TERMINATED. El estado no distingue el éxito del fallo, y esa es exactamente la trampa de 08-02: join() retorna con normalidad tanto si el hilo hizo su trabajo como si murió por una excepción. Para distinguirlos necesitas o bien un manejador de excepciones no capturadas, o bien un Future (08-05), que sí guarda la excepción y te la relanza en get().
RUNNABLE: el matiz que confunde a todo el mundo
RUNNABLE: el matiz que confunde a todo el mundoMuchos manuales dibujan un estado RUNNING separado de RUNNABLE. Java no lo tiene. Thread.State.RUNNABLE agrupa dos situaciones muy distintas:
- Ejecutando ahora mismo en un núcleo.
- Listo para ejecutar, en la cola del planificador, esperando que le den un núcleo.
La JVM no las distingue porque no puede: quién ejecuta en cada instante lo decide el planificador del sistema operativo, sin informar a la JVM. Con ocho hilos RUNNABLE en una máquina de cuatro núcleos, en cualquier instante hay a lo sumo cuatro ejecutando de verdad y al menos cuatro esperando turno, y todos aparecen como RUNNABLE.
Hay una tercera situación, aún más contraintuitiva:
Un hilo bloqueado en E/S clásica aparece como
RUNNABLE.
Un hilo parado en InputStream.read() esperando al disco lleva un rato sin ejecutar ni una instrucción, pero getState() dice RUNNABLE. La razón es que la JVM no tiene ni idea de que ese hilo está aparcado dentro de una llamada al sistema: desde su punto de vista, el hilo está ejecutando código nativo.
// Este hilo puede pasar 30 segundos aqui y su estado sera RUNNABLE
// todo el tiempo, aunque no consuma ni un ciclo de CPU.
Thread lector = new Thread(() -> {
try (BufferedReader br = Files.newBufferedReader(Path.of("datos/enorme.csv"))) {
String linea;
while ((linea = br.readLine()) != null) { procesar(linea); }
} catch (IOException e) { /* ... */ }
}, "bibliotech-lector");Consecuencia práctica para diagnosticar: ver muchos hilos RUNNABLE no significa que la CPU esté saturada. Hay que mirar la traza de pila:
- Si la cima es
java.net.SocketInputStream.socketRead0oFileInputStream.readBytes, ese hilo está esperando E/S, no calculando. - Si la cima es código tuyo en un bucle, entonces sí está consumiendo CPU.
Esta distinción es la que separa "necesito más núcleos" de "necesito más hilos porque estoy esperando al disco" —la tabla de tareas limitadas por CPU y por E/S de 08-01, vista ahora desde el otro lado—.
BLOCKED frente a WAITING: la distinción que hace legible un volcado
BLOCKED frente a WAITING: la distinción que hace legible un volcadoEsta es la distinción más valiosa de la lección para el trabajo real.
BLOCKED |
WAITING |
|
|---|---|---|
| Qué espera | Un monitor (la llave de un synchronized) |
Una señal de otro hilo |
| Cómo se llega | Intentar entrar en un synchronized ocupado |
wait(), join(), LockSupport.park() |
| Cómo se sale | El dueño del monitor lo suelta | Otro hilo llama a notify/notifyAll, termina el hilo esperado, o llega una interrupción |
| ¿Depende de otro hilo? | Sí, del que tiene el monitor | Sí, del que debe señalizar |
| En un volcado | waiting to lock <0x...> |
in Object.wait() / parking to wait for <0x...> |
| Qué suele indicar | Contención: muchos hilos peleando por un candado | Coordinación: cola vacía, tarea pendiente, join |
| Si dura mucho | Sospecha de interbloqueo o sección crítica larga | Normal en un pool ocioso; sospechoso si nadie va a señalizar |
La forma corta de recordarlo:
BLOCKED= "no me dejan entrar".WAITING= "estoy esperando a que me avisen".
Demostración de los dos estados a la vez:
import java.util.concurrent.TimeUnit;
public class BloqueadoVsEsperando {
private static final Object candado = new Object();
public static void main(String[] args) throws InterruptedException {
// Hilo 1: toma el candado y se queda dentro haciendo wait().
// Suelta el monitor al entrar en wait(), pero queda WAITING.
Thread esperador = new Thread(() -> {
synchronized (candado) {
try {
candado.wait(); // WAITING (y SUELTA el monitor)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "hilo-esperador");
// Hilo 2: toma el candado y lo retiene durmiendo.
// sleep NO suelta el monitor (08-02, apartado 8).
Thread acaparador = new Thread(() -> {
synchronized (candado) {
try {
TimeUnit.SECONDS.sleep(5); // TIMED_WAITING, con el monitor
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "hilo-acaparador");
// Hilo 3: quiere el candado y no lo consigue -> BLOCKED.
Thread bloqueado = new Thread(() -> {
synchronized (candado) {
System.out.println("[bloqueado] por fin he entrado");
}
}, "hilo-bloqueado");
esperador.start();
TimeUnit.MILLISECONDS.sleep(200); // que llegue a wait()
acaparador.start();
TimeUnit.MILLISECONDS.sleep(200); // que tome el candado
bloqueado.start();
TimeUnit.MILLISECONDS.sleep(200); // que choque con el candado
System.out.println("esperador : " + esperador.getState());
System.out.println("acaparador : " + acaparador.getState());
System.out.println("bloqueado : " + bloqueado.getState());
// Limpieza
acaparador.interrupt();
synchronized (candado) { candado.notifyAll(); }
}
}Salida:
Los tres estados a la vez, y cada uno por un motivo distinto. Fíjate en el detalle crucial de esperador: entró en el synchronized, o sea, tenía el monitor, y al llamar a wait() lo soltó. Por eso acaparador pudo entrar después. Esta es la diferencia esencial entre wait() y sleep():
Thread.sleep(ms) |
objeto.wait() |
|
|---|---|---|
| ¿Suelta el monitor? | No | Sí |
| Dónde se puede llamar | En cualquier sitio | Solo dentro de synchronized sobre ese objeto |
| Cómo despierta | Plazo agotado o interrupción | notify/notifyAll, plazo, interrupción o espurio |
| Estado resultante | TIMED_WAITING |
WAITING o TIMED_WAITING |
| Para qué sirve | Pausar | Coordinar |
TIMED_WAITING: esperar con plazo
TIMED_WAITING: esperar con plazoTIMED_WAITING es la versión con plazo de WAITING. Los métodos que llevan a él:
Thread.sleep(1000); // TIMED_WAITING
TimeUnit.SECONDS.sleep(1); // idem
objeto.wait(1000); // TIMED_WAITING, sueltas el monitor
hilo.join(1000); // TIMED_WAITING
LockSupport.parkNanos(1_000_000_000L); // TIMED_WAITING
bloqueo.tryLock(1, TimeUnit.SECONDS); // TIMED_WAITING (08-04)
cola.poll(1, TimeUnit.SECONDS); // TIMED_WAITING (08-06)En un volcado, TIMED_WAITING (sleeping) y TIMED_WAITING (on object monitor) se distinguen entre sí, lo cual es útil: el primero es un hilo que decidió pausarse, el segundo es un hilo esperando una señal con plazo.
El detalle de diagnóstico que salva horas: un hilo permanentemente en TIMED_WAITING (sleeping) dentro de un bucle es casi siempre un sondeo (polling), y casi siempre es un diseño mejorable:
// ANTIPATRON: sondeo. El hilo despierta 10 veces por segundo
// solo para comprobar si hay algo, y normalmente no lo hay.
while (colaReservas.isEmpty()) {
Thread.sleep(100);
}
Reserva r = colaReservas.poll();
// CORRECTO: bloquearse hasta que haya algo, sin despertar en vano (08-06).
Reserva r = colaReservas.take(); // BlockingQueue
wait, notify y notifyAll
wait, notify y notifyAllEstos tres métodos no están en Thread: están en Object, la clase de 03-09. Cualquier objeto de Java puede servir de punto de coordinación, porque cualquier objeto tiene un monitor asociado.
| Método | Qué hace |
|---|---|
objeto.wait() |
Suelta el monitor de objeto y deja el hilo en WAITING hasta recibir una señal |
objeto.wait(ms) |
Igual, pero despierta también al agotarse el plazo (TIMED_WAITING) |
objeto.notify() |
Despierta un hilo (arbitrario) de los que esperan en objeto |
objeto.notifyAll() |
Despierta todos los hilos que esperan en objeto |
La regla número uno, que el compilador no comprueba:
Los tres métodos deben llamarse con el monitor de ese objeto en la mano, es decir, dentro de un bloque o método
synchronizedsobre ese mismo objeto. Si no, se lanzaIllegalMonitorStateExceptionen tiempo de ejecución.
Object candado = new Object();
// MAL: sin el monitor -> IllegalMonitorStateException
candado.wait();
// BIEN
synchronized (candado) {
candado.wait();
}Por qué esa regla existe —y es una pregunta de entrevista clásica—: sin ella habría una condición de carrera irreparable. Imagina que pudieras hacer wait() sin el monitor:
- El consumidor comprueba
cola.isEmpty()→true. - Justo aquí el productor añade un elemento y llama a
notify(). - El consumidor llama a
wait()… y se pierde la señal, porque llegó antes de que empezara a esperar.
Resultado: el consumidor espera para siempre con la cola llena. Exigir el monitor hace que "comprobar la condición" y "empezar a esperar" sean una sola operación indivisible desde el punto de vista del productor, porque este necesita el mismo monitor para señalizar.
Ejemplo mínimo de coordinación:
import java.util.ArrayDeque;
import java.util.Deque;
public class CoordinacionBasica {
// El objeto de bloqueo Y de senalizacion: la propia cola.
private static final Deque<String> reservas = new ArrayDeque<>();
public static void main(String[] args) throws InterruptedException {
Thread consumidor = new Thread(() -> {
try {
synchronized (reservas) {
// BUCLE while, no if. Apartado 8.
while (reservas.isEmpty()) {
System.out.println("[consumidor] cola vacia, espero");
reservas.wait(); // suelta el monitor y espera
}
System.out.println("[consumidor] atiendo: " + reservas.poll());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "bibliotech-consumidor");
Thread productor = new Thread(() -> {
try {
Thread.sleep(500);
synchronized (reservas) {
reservas.add("Reserva de Marta Ruiz: 978-0000000001");
System.out.println("[productor] reserva anadida, aviso");
reservas.notifyAll(); // despierta a los que esperan
} // el monitor se suelta AQUI
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "bibliotech-productor");
consumidor.start();
productor.start();
consumidor.join();
productor.join();
}
}Salida:
[consumidor] cola vacia, espero
[productor] reserva anadida, aviso
[consumidor] atiendo: Reserva de Marta Ruiz: 978-0000000001Un detalle que se pasa por alto: notifyAll() no suelta el monitor. El hilo señalizador sigue dentro del synchronized hasta la llave de cierre. El consumidor despierta, pero no puede continuar hasta que el monitor quede libre; mientras tanto pasa de WAITING a BLOCKED. Es la transición del apartado 2 —todo pasa por RUNNABLE— vista en acción, y la razón por la que conviene señalizar al final del bloque, no al principio.
Nota de precedencia.
synchronizedse estudia a fondo en 08-04: qué es exactamente un monitor, la reentrada, el bloqueo privado y la granularidad. Aquí basta con la regla operativa: para llamar await,notifyonotifyAllsobre un objeto, hay que estar dentro de unsynchronizedsobre ese mismo objeto.
- El bucle
while obligatorio
while obligatorioEsta es la regla que más se incumple y la que produce errores más difíciles de reproducir:
wait()va SIEMPRE dentro de un buclewhileque comprueba la condición. Nunca dentro de unif.
// MAL - un bug real, no una cuestion de estilo
synchronized (reservas) {
if (reservas.isEmpty()) {
reservas.wait();
}
Reserva r = reservas.poll(); // puede ser null
atender(r); // NullPointerException
}
// BIEN
synchronized (reservas) {
while (reservas.isEmpty()) {
reservas.wait();
}
Reserva r = reservas.poll(); // garantizado no nulo
atender(r);
}Hay tres motivos independientes, y cada uno bastaría por sí solo:
Motivo 1: despertares espurios. La especificación de Java permite explícitamente que wait() retorne sin que nadie haya llamado a notify. No es un fallo de implementación: es una concesión deliberada a cómo funcionan las primitivas de sincronización de los sistemas operativos subyacentes (pthread_cond_wait tiene la misma advertencia). Son raros, pero ocurren, y ocurren más en máquinas cargadas —o sea, en producción y no en tu portátil—.
Motivo 2: notifyAll despierta a todos, pero solo uno puede ganar. Con tres consumidores esperando y un solo elemento en la cola, notifyAll() despierta a los tres. El primero en readquirir el monitor se lleva el elemento; los otros dos vuelven a comprobar y —con while— vuelven a esperar. Con if, siguen adelante y encuentran la cola vacía.
Motivo 3: la condición puede haber cambiado entre la señal y el despertar. Entre notify() y el momento en que el hilo despertado readquiere el monitor pasa tiempo, y en ese tiempo otro hilo puede haber entrado y consumido el elemento. La señal dice "algo cambió", no "tu condición se cumple ahora".
El tercer motivo es el más importante y el que hace del while una necesidad lógica, no una precaución: notify no transmite información sobre el estado, solo dice "vuelve a mirar". La única fuente de verdad es la condición, y por eso hay que reevaluarla.
Demostración del fallo con if:
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.TimeUnit;
public class ElBugDelIf {
private static final Deque<Integer> cola = new ArrayDeque<>();
static Runnable consumidorConIf = () -> {
try {
synchronized (cola) {
if (cola.isEmpty()) { // BUG: deberia ser while
cola.wait();
}
Integer v = cola.poll();
System.out.println("[" + Thread.currentThread().getName()
+ "] consume: " + v
+ (v == null ? " <-- FALLO: cola vacia" : ""));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
public static void main(String[] args) throws InterruptedException {
// Tres consumidores esperando...
for (int i = 1; i <= 3; i++) {
new Thread(consumidorConIf, "consumidor-" + i).start();
}
TimeUnit.MILLISECONDS.sleep(300);
// ...y UN solo elemento con notifyAll.
synchronized (cola) {
cola.add(42);
cola.notifyAll(); // despierta a los TRES
}
TimeUnit.MILLISECONDS.sleep(500);
}
}Salida:
[consumidor-1] consume: 42
[consumidor-2] consume: null <-- FALLO: cola vacia
[consumidor-3] consume: null <-- FALLO: cola vaciaDos de los tres consumidores obtienen null y en un programa real eso sería un NullPointerException en la siguiente línea. Cambia if por while y ejecuta de nuevo: los consumidores 2 y 3 vuelven a esperar en silencio, que es exactamente lo correcto.
notify frente a notifyAll
notify frente a notifyAllnotify() |
notifyAll() |
|
|---|---|---|
| A cuántos despierta | A uno, elegido arbitrariamente | A todos los que esperan en ese objeto |
| Coste | Menor | Mayor: todos despiertan y compiten por el monitor |
| Riesgo | Señal perdida: puede despertar al hilo equivocado | Ninguno de corrección; solo ineficiencia |
| Cuándo es seguro | Todos los que esperan son intercambiables y esperan la misma condición | Siempre |
La recomendación es tajante: usa notifyAll() salvo que puedas demostrar que es seguro usar notify().
El peligro de notify() es el despertar perdido. Si en el mismo objeto esperan hilos por condiciones distintas —unos porque la cola está vacía, otros porque está llena—, notify() puede despertar precisamente a uno que no puede avanzar. Ese hilo comprueba su condición, sigue sin cumplirse, vuelve a wait()… y la señal se ha consumido. El hilo que sí podía avanzar nunca fue avisado, y el sistema se queda parado sin ningún error ni excepción.
// PELIGROSO: dos condiciones distintas esperando en el MISMO objeto
synchronized (buffer) {
while (buffer.estaLleno()) buffer.wait(); // productores
// ...
buffer.notify(); // puede despertar a OTRO productor
}
synchronized (buffer) {
while (buffer.estaVacio()) buffer.wait(); // consumidores
// ...
buffer.notify(); // puede despertar a OTRO consumidor
}En ese diseño, notify() puede acabar en un bloqueo total del sistema. Con notifyAll() el problema desaparece: todos comprueban, los que pueden avanzan, los que no vuelven a esperar. Se paga un poco de coste por una garantía de corrección; casi siempre es el mejor cambio posible.
La solución elegante —una espera por cada condición— existe: son los objetos
ConditiondeReentrantLock, que permitensignal()sobre exactamente el grupo correcto de hilos. Se ven en 08-04.
yield y onSpinWait
yield y onSpinWaitDos métodos menores, en una nota.
Thread.yield() es una sugerencia al planificador: "si hay otro hilo listo, dale paso". No garantiza nada; el planificador puede ignorarlo por completo, y el hilo puede volver a ser elegido inmediatamente. No cambia el estado (sigue RUNNABLE) y no suelta ningún monitor. No lo uses para corrección; su lugar legítimo son las pruebas de estrés, donde forzar más cambios de contexto ayuda a que aparezcan las condiciones de carrera latentes.
// Uso legitimo: en una prueba, para provocar entrelazados distintos
// y aumentar la probabilidad de que un bug de carrera se manifieste.
for (int i = 0; i < 1000; i++) {
contador.incrementar();
if (i % 10 == 0) Thread.yield();
}Thread.onSpinWait() (Java 9) es una pista para la CPU dentro de una espera activa muy corta: emite una instrucción como PAUSE en x86, que reduce el consumo energético y mejora el rendimiento al salir del bucle. Solo tiene sentido en bucles de espera de nanosegundos y en código de muy bajo nivel:
// Espera activa MUY corta, en codigo de alto rendimiento.
// En codigo de aplicacion normal esto es un antipatron:
// consume un nucleo entero. Usa wait/notify o una BlockingQueue.
while (!listo) {
Thread.onSpinWait();
}Ninguno de los dos resuelve un problema de coordinación. Si crees que necesitas yield() para que tu programa funcione, lo que necesitas es sincronización.
- Los métodos retirados:
stop, suspend, resume
stop, suspend, resumeThread tiene tres métodos que hacían exactamente lo que uno querría —parar un hilo, pausarlo y reanudarlo— y que están marcados como obsoletos desde Java 1.2 (1998). En Java 20 stop() pasó a lanzar UnsupportedOperationException siempre, y suspend/resume fueron marcados para eliminación. Vale la pena entender por qué, porque el motivo enseña algo real.
Thread.stop(): mataba el hilo lanzándole un ThreadDeath en el punto en que estuviera.
El problema: suelta todos sus monitores instantáneamente, en mitad de lo que estuviera haciendo.
// Supongamos que el hilo esta AQUI cuando le llega stop():
synchronized (catalogo) {
catalogo.eliminarIsbn(isbn); // ya se ha ejecutado
// <-- stop() cae justo aqui
catalogo.eliminarDelIndice(isbn); // NUNCA se ejecuta
}Resultado: el ISBN se quitó del conjunto pero no del índice. El catálogo queda incoherente, el monitor se libera y los demás hilos entran tan tranquilos a leer una estructura corrupta. Y no hay ninguna excepción ni traza que lo indique: el fallo aparece más tarde, en otro sitio, sin relación aparente.
Es exactamente el problema que resuelve la cancelación cooperativa de 08-02: interrupt() no interrumpe donde le da la gana, sino en los puntos que el propio hilo ha designado, donde el estado es coherente.
Thread.suspend() y resume(): pausaban y reanudaban un hilo.
El problema: suspend() no suelta los monitores. Un hilo suspendido dentro de un synchronized mantiene el candado indefinidamente. Si el hilo que iba a llamar a resume() necesita ese mismo candado para llegar hasta ahí, interbloqueo perfecto, sin error ni traza.
// La receta del interbloqueo:
hiloTrabajador.suspend(); // suspendido reteniendo el monitor de 'catalogo'
synchronized (catalogo) { // main queda BLOCKED aqui, para siempre
hiloTrabajador.resume(); // esta linea nunca se alcanza
}Qué se usa en su lugar:
| Método retirado | Sustituto |
|---|---|
stop() |
interrupt() + protocolo cooperativo (08-02) |
suspend() / resume() |
Bandera volatile + wait/notify, o Semaphore/CyclicBarrier (08-05) |
destroy() |
Nunca llegó a implementarse; eliminado |
Pausa y reanudación bien hechas:
public class TareaPausable implements Runnable {
private final Object monitor = new Object();
private boolean pausada = false; // protegido por 'monitor'
private volatile boolean detenida = false;
public void pausar() { synchronized (monitor) { pausada = true; } }
public void reanudar() {
synchronized (monitor) {
pausada = false;
monitor.notifyAll(); // despierta al trabajador
}
}
public void detener() { detenida = true; }
@Override
public void run() {
try {
while (!detenida && !Thread.currentThread().isInterrupted()) {
// PUNTO DE PAUSA elegido por el propio hilo: aqui el
// estado es coherente y no se retiene ningun candado
// de negocio. Nada que ver con suspend().
synchronized (monitor) {
while (pausada && !detenida) {
monitor.wait(); // suelta 'monitor', queda WAITING
}
}
procesarUnLote();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void procesarUnLote() { /* trabajo */ }
}La diferencia esencial con suspend(): el hilo elige dónde se pausa. En ese punto el estado del negocio es coherente y no se retiene ningún candado del catálogo. Es el mismo principio que la interrupción cooperativa.
- Diagnóstico en la práctica: el volcado de hilos
Llegamos a la parte que usarás de verdad. Un volcado de hilos (thread dump) es una foto instantánea de todos los hilos de la JVM: su nombre, su estado, su traza de pila y los candados que tienen o esperan. Es la herramienta número uno para diagnosticar una aplicación colgada o lenta.
12.1 Cómo obtenerlo
Opción A: Ctrl+\ en el terminal (Linux/macOS; en Windows, Ctrl+Break). La JVM imprime el volcado en System.err. No requiere herramientas y no mata el proceso.
Opción B: jstack, incluido en el JDK. Es la opción profesional:
# 1. Localizar el proceso Java
jps -l
# 48213 com.nexussoftware.bibliotech.BiblioTechApp
# 48291 jdk.jcmd/sun.tools.jps.Jps
# 2. Volcar sus hilos
jstack 48213
# 3. Guardarlo para analizarlo con calma
jstack 48213 > /tmp/volcado-1.txt
# 4. Tomar TRES volcados separados por 5 segundos:
# comparar los tres distingue un hilo atascado de uno que avanza
for i in 1 2 3; do jstack 48213 > /tmp/volcado-$i.txt; sleep 5; doneOpción C: jcmd, más moderno y con más información:
El consejo operativo que más vale de todo el apartado: toma siempre TRES volcados separados por unos segundos. Un volcado aislado te dice dónde está cada hilo en ese instante, y un hilo puede estar de paso. Tres volcados donde el mismo hilo aparece en la misma línea significan que está atascado, no ocupado. Es la diferencia entre un diagnóstico y una conjetura.
12.2 Anatomía de una entrada
"bibliotech-importador" #21 prio=5 os_prio=0 cpu=1243.55ms elapsed=18.32s tid=0x00007f3c1c0e1800 nid=0x5f2a waiting for monitor entry [0x00007f3c0a1fe000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nexussoftware.bibliotech.servicio.Catalogo.anadir(Catalogo.java:88)
- waiting to lock <0x000000071ab34d18> (a com.nexussoftware.bibliotech.servicio.Catalogo)
at com.nexussoftware.bibliotech.persistencia.TareaImportacion.run(TareaImportacion.java:64)
at java.base/java.lang.Thread.run(Thread.java:1583)Campo a campo:
| Campo | Significado |
|---|---|
"bibliotech-importador" |
El nombre del hilo. Aquí se cobra el consejo de 08-02 |
#21 |
Identificador del hilo en la JVM |
prio=5 |
Prioridad Java (recuerda 08-02: apenas significa nada) |
cpu=1243.55ms |
CPU consumida en total. Un hilo "atascado" con CPU creciente está en un bucle, no bloqueado |
elapsed=18.32s |
Tiempo de vida del hilo |
nid=0x5f2a |
Identificador nativo, en hexadecimal. Se cruza con top -H para ver qué hilo quema CPU |
waiting for monitor entry |
Resumen textual del estado |
java.lang.Thread.State: BLOCKED |
El estado del apartado 1 |
- waiting to lock <0x...> |
Qué candado espera, con su identidad y su clase |
at ... |
La traza de pila: dónde está exactamente |
Las tres líneas que hay que mirar primero, en este orden: el estado, el - waiting to lock / - locked, y la primera línea at de tu propio código (ignorando las de java.base).
Formas habituales:
--- Un hilo TRABAJANDO de verdad (consume CPU) ---
"bibliotech-calculo" #24 ... cpu=8420.11ms ... runnable
java.lang.Thread.State: RUNNABLE
at com.nexussoftware.bibliotech.servicio.CalculadoraMultas.recalcular(CalculadoraMultas.java:52)
--- Un hilo esperando E/S: RUNNABLE pero SIN consumir CPU ---
"bibliotech-lector" #25 ... cpu=12.30ms ... runnable
java.lang.Thread.State: RUNNABLE
at java.base/java.io.FileInputStream.readBytes(Native Method)
at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:...)
--- Un hilo de pool OCIOSO: normal, no es un problema ---
"bibliotech-avisos-3" #31 ... cpu=340.02ms ... waiting on condition
java.lang.Thread.State: WAITING (parking)
- parking to wait for <0x000000071b0c4470> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.base/java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:...)
at java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:...)
--- Un hilo DORMIDO ---
"bibliotech-monitor" #33 ... sleeping
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.base/java.lang.Thread.sleep(Native Method)Cómo leer estos cuatro patrones:
RUNNABLEconcpualta y creciente entre volcados: trabajando o en un bucle infinito. Compara los tres volcados: si la traza no cambia y la CPU sube, es un bucle sin salida.RUNNABLEconcpubaja yreadBytesen la cima: esperando E/S. No es un problema de CPU; es el caso de tarea limitada por E/S de 08-01.WAITING (parking)engetTaskde unThreadPoolExecutor: hilo de pool esperando trabajo. Es lo normal, no lo persigas. Los verás en 08-05 a decenas.BLOCKED (on object monitor)en varios hilos sobre el mismo<0x...>: contención. Si además dura entre los tres volcados, busca quién posee ese candado —aparecerá con- locked <ese mismo 0x...>— y por qué tarda tanto.
- Leer un interbloqueo detectado por la JVM
Lo mejor de los volcados es que la JVM detecta los interbloqueos de monitores por ti y los explica al final del volcado. Este es el aspecto que tiene, con código de BiblioTech:
public class InterbloqueoBiblioTech {
private static final Object CATALOGO = new Object();
private static final Object REGISTRO = new Object();
public static void main(String[] args) {
// Hilo A: catalogo -> registro
new Thread(() -> {
synchronized (CATALOGO) {
dormir(200);
synchronized (REGISTRO) {
System.out.println("A: nunca llega aqui");
}
}
}, "bibliotech-prestamos").start();
// Hilo B: registro -> catalogo (ORDEN INVERSO: la causa)
new Thread(() -> {
synchronized (REGISTRO) {
dormir(200);
synchronized (CATALOGO) {
System.out.println("B: nunca llega aqui");
}
}
}, "bibliotech-devoluciones").start();
}
static void dormir(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}Este programa se cuelga: no imprime nada, no lanza ninguna excepción, no consume CPU y no termina nunca. Es el peor tipo de fallo: silencioso y total. Lanza jstack sobre él y el final del volcado dice:
Found one Java-level deadlock:
=============================
"bibliotech-prestamos":
waiting to lock monitor 0x00007f3c14006e00 (object 0x000000071ab34d18, a java.lang.Object),
which is held by "bibliotech-devoluciones"
"bibliotech-devoluciones":
waiting to lock monitor 0x00007f3c14009480 (object 0x000000071ab34d28, a java.lang.Object),
which is held by "bibliotech-prestamos"
Java stack information for the threads listed above:
===================================================
"bibliotech-prestamos":
at InterbloqueoBiblioTech.lambda$main$0(InterbloqueoBiblioTech.java:14)
- waiting to lock <0x000000071ab34d18> (a java.lang.Object)
- locked <0x000000071ab34d28> (a java.lang.Object)
at java.base/java.lang.Thread.run(Thread.java:1583)
"bibliotech-devoluciones":
at InterbloqueoBiblioTech.lambda$main$1(InterbloqueoBiblioTech.java:25)
- waiting to lock <0x000000071ab34d28> (a java.lang.Object)
- locked <0x000000071ab34d18> (a java.lang.Object)
at java.base/java.lang.Thread.run(Thread.java:1583)
Found 1 deadlock.Cómo se lee, en tres pasos:
Found one Java-level deadlockes la palabra clave a buscar. Si aparece, no hay nada que discutir: hay un interbloqueo.- La sección de arriba da el ciclo:
prestamosespera un candado que tienedevoluciones, ydevolucionesespera un candado que tieneprestamos. Un ciclo de dos; pueden ser de tres o más. - Las trazas de pila dan las líneas exactas —
InterbloqueoBiblioTech.java:14y:25— y, sobre todo, la combinación- locked <A>+- waiting to lock <B>en un hilo y- locked <B>+- waiting to lock <A>en el otro. Eso es la firma del interbloqueo por orden inverso de adquisición, y la solución —imponer un orden global— es 08-04.
Dos limitaciones importantes de la detección automática:
- Detecta interbloqueos de monitores
synchronizedy deReentrantLock(a través deThreadMXBean), pero no detecta esperas mutuas lógicas: dos hilos esperando cada uno una señal del otro conwait()no producen un "deadlock" declarado, aunque el efecto sea el mismo. Ahí solo verás dos hilos enWAITINGpara siempre. - No detecta el livelock (08-04), en el que los hilos sí avanzan pero sin progresar.
- Inspección desde el propio programa
A veces conviene inspeccionar los hilos desde dentro de la aplicación, típicamente para un endpoint de diagnóstico o para volcar el estado antes de abortar.
Thread.getAllStackTraces() devuelve un mapa de todos los hilos vivos con sus trazas:
import java.util.Map;
public class VolcadoInterno {
/** Vuelca todos los hilos vivos y sus trazas. Util en un manejador global. */
public static void volcarHilos() {
Map<Thread, StackTraceElement[]> todos = Thread.getAllStackTraces();
System.out.println("=== VOLCADO INTERNO: " + todos.size() + " hilos ===");
todos.forEach((hilo, traza) -> {
System.out.printf("%n\"%s\" id=%d estado=%s demonio=%s%n",
hilo.getName(), hilo.threadId(), hilo.getState(), hilo.isDaemon());
// Solo las 5 primeras lineas: lo demas rara vez aporta
int n = Math.min(5, traza.length);
for (int i = 0; i < n; i++) {
System.out.println(" at " + traza[i]);
}
if (traza.length > n) {
System.out.println(" ... " + (traza.length - n) + " mas");
}
});
}
}ThreadMXBean (de java.lang.management) da mucha más información, incluida la detección programática de interbloqueos:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DetectorInterbloqueos {
private static final ThreadMXBean MX = ManagementFactory.getThreadMXBean();
/** Devuelve un informe de interbloqueo, o null si no hay ninguno. */
public static String detectar() {
long[] ids = MX.findDeadlockedThreads(); // null si no hay
if (ids == null) {
return null;
}
StringBuilder sb = new StringBuilder("INTERBLOQUEO DETECTADO:\n");
for (ThreadInfo info : MX.getThreadInfo(ids, true, true)) {
sb.append(" Hilo '").append(info.getThreadName())
.append("' (").append(info.getThreadState()).append(")\n")
.append(" espera : ").append(info.getLockName()).append('\n')
.append(" en manos de: ").append(info.getLockOwnerName()).append('\n');
}
return sb.toString();
}
/**
* Vigilante que comprueba cada 10 segundos si hay interbloqueo.
* Demonio: es puramente informativo y no debe impedir el cierre (08-02).
*/
public static void arrancarVigilante() {
Thread vigilante = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
String informe = detectar();
if (informe != null) {
System.err.println(informe);
}
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "bibliotech-vigilante-interbloqueos");
vigilante.setDaemon(true);
vigilante.start();
}
}Un vigilante así, registrando en el logger de 06-07, convierte un "la aplicación se ha colgado y no sabemos por qué" en una entrada de log que dice exactamente qué dos hilos y qué dos candados. Merece las treinta líneas que cuesta.
Herramientas gráficas.
jconsole(incluida en el JDK) y VisualVM (descarga aparte) muestran en tiempo real el número de hilos, su estado, un gráfico de actividad y un botón de "Detectar interbloqueo". Para una primera exploración son mucho más cómodas quejstack; para un análisis serio o para un sistema sin interfaz gráfica,jstacksigue siendo la herramienta. Java Flight Recorder + JDK Mission Control es el escalón profesional, con muestreo continuo y bajo impacto.
- BiblioTech: seguir el estado de sus hilos
Aplicación práctica: un monitor que muestra el estado de los hilos de BiblioTech mientras trabajan.
package com.nexussoftware.bibliotech.infraestructura;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Monitor de hilos de BiblioTech.
*
* Es un hilo DEMONIO: puramente informativo, no debe impedir el cierre
* de la aplicacion (08-02, apartado 7).
*/
public class MonitorHilos implements Runnable {
private final List<Thread> vigilados;
private final long periodoMs;
public MonitorHilos(List<Thread> vigilados, long periodoMs) {
this.vigilados = List.copyOf(vigilados); // copia inmutable: seguro
this.periodoMs = periodoMs;
}
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
boolean algunoVivo = false;
StringBuilder sb = new StringBuilder("[monitor] ");
for (Thread h : vigilados) {
Thread.State e = h.getState();
sb.append(String.format("%-24s %-14s | ", h.getName(), e));
if (e != Thread.State.TERMINATED && e != Thread.State.NEW) {
algunoVivo = true;
}
}
System.out.println(sb);
if (!algunoVivo) {
System.out.println("[monitor] todos los hilos han terminado");
return;
}
TimeUnit.MILLISECONDS.sleep(periodoMs);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/** Arranca el monitor como demonio y devuelve su hilo. */
public static Thread arrancar(List<Thread> vigilados, long periodoMs) {
Thread t = new Thread(new MonitorHilos(vigilados, periodoMs),
"bibliotech-monitor-hilos");
t.setDaemon(true);
t.start();
return t;
}
}Uso, sobre la importación de 08-02 y una tarea de avisos:
package com.nexussoftware.bibliotech.presentacion;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class DemostracionEstados {
public static void main(String[] args) throws InterruptedException {
Catalogo catalogo = new Catalogo();
Thread importador = new Thread(
new TareaImportacion(Path.of("datos/inventario.csv"), catalogo),
"bibliotech-importador");
Thread avisos = new Thread(() -> {
for (int i = 1; i <= 20; i++) {
try {
// E/S simulada: en un volcado, este hilo apareceria
// en TIMED_WAITING (sleeping).
TimeUnit.MILLISECONDS.sleep(150);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "bibliotech-avisos");
// El monitor arranca ANTES: asi captura el estado NEW.
MonitorHilos.arrancar(List.of(importador, avisos), 400);
TimeUnit.MILLISECONDS.sleep(500);
importador.start();
avisos.start();
importador.join();
avisos.join();
TimeUnit.MILLISECONDS.sleep(500); // margen para la ultima linea
}
}Salida:
[monitor] bibliotech-importador NEW | bibliotech-avisos NEW |
[monitor] bibliotech-importador RUNNABLE | bibliotech-avisos TIMED_WAITING |
[monitor] bibliotech-importador TIMED_WAITING | bibliotech-avisos TIMED_WAITING |
[monitor] bibliotech-importador RUNNABLE | bibliotech-avisos TIMED_WAITING |
[monitor] bibliotech-importador TERMINATED | bibliotech-avisos TIMED_WAITING |
[monitor] bibliotech-importador TERMINATED | bibliotech-avisos TERMINATED |
[monitor] todos los hilos han terminadoLo que esta salida enseña:
- El importador alterna entre
RUNNABLE(leyendo y validando líneas) yTIMED_WAITING(las pausas simuladas). Es el perfil de una tarea mixta. - El hilo de avisos está casi siempre en
TIMED_WAITING: es el perfil de una tarea limitada por E/S. Esta es exactamente la observación empírica que justifica usar muchos más hilos que núcleos para los avisos (08-01, apartado 6) y que se explotará en 08-05. - El monitor detecta el fin de los dos y termina solo. Como es demonio, aunque no lo hiciera no impediría el cierre de la JVM.
Errores Comunes y Consejos
Error 1: creer que RUNNABLE significa "consumiendo CPU". Un hilo bloqueado en read() aparece como RUNNABLE y no consume nada. Mira la traza de pila y el campo cpu= del volcado, no solo el estado.
Error 2: usar if en lugar de while alrededor de wait(). Un bug, no un estilo. Despertares espurios, notifyAll con varios esperando, y condiciones que cambian entre la señal y el despertar: tres motivos independientes.
Error 3: llamar a wait/notify sin el monitor. IllegalMonitorStateException en tiempo de ejecución. El compilador no ayuda aquí.
Error 4: usar notify() cuando hay hilos esperando por condiciones distintas. Señal perdida y bloqueo silencioso. Usa notifyAll(), o Condition de 08-04 si el rendimiento lo justifica.
Error 5: llamar a notifyAll() al principio del bloque sincronizado. No suelta el monitor: los despertados pasan a BLOCKED y esperan igual. Señaliza lo más cerca posible del final del bloque.
Error 6: usar stop, suspend o resume. Estado corrupto e interbloqueos garantizados. stop() ni siquiera funciona ya en Java 20+.
Error 7: sacar un único volcado de hilos y sacar conclusiones. Un hilo puede estar de paso por esa línea. Toma tres, separados por cinco segundos, y compara.
Error 8: perseguir hilos WAITING en un pool. Los hilos ociosos de un ThreadPoolExecutor están en WAITING (parking) dentro de getTask(). Es su estado normal cuando no hay trabajo.
Error 9: sondear con sleep en un bucle. Consume CPU para nada, y añade latencia igual al periodo de sondeo. Bloquéate en la condición: wait, BlockingQueue o CountDownLatch.
Consejo 1: aprende a leer un volcado antes de necesitarlo. Provoca un interbloqueo a propósito con el código del apartado 13 y lánzale jstack. Cuando ocurra en producción a las tres de la mañana, agradecerás haberlo visto antes.
Consejo 2: cruza nid del volcado con top -H -p <pid>. top -H muestra los hilos con su consumo de CPU en decimal; conviértelo a hexadecimal y búscalo como nid= en el volcado. Así identificas exactamente qué hilo Java está quemando un núcleo.
Consejo 3: instala un detector de interbloqueos con ThreadMXBean. Treinta líneas y un hilo demonio convierten un cuelgue misterioso en una entrada de log concreta.
Consejo 4: no uses wait/notify en código nuevo. Úsalo para entender y para leer código ajeno. Para escribir, usa BlockingQueue (08-06), CountDownLatch o Semaphore (08-05): hacen lo mismo, sin los tres errores clásicos de este apartado.
Ejercicios
Ejercicio 1: Observador de estados
Escribe ObservadorEstados que lance un hilo llamado hilo-bajo-observacion cuyo trabajo pase deliberadamente por al menos cuatro estados distintos: TIMED_WAITING (durmiendo), BLOCKED (por un candado que main retiene), WAITING (por un wait() sin plazo) y TERMINATED. Un segundo hilo, demonio y llamado observador, debe muestrear el estado del primero cada 50 ms e imprimir solo los cambios de estado (no repetir el mismo estado en líneas consecutivas), junto con el tiempo transcurrido desde el arranque medido con System.nanoTime().
Ejercicio 2: Cola de reservas con wait/notifyAll
Implementa ColaReservasCoordinada, una cola acotada de reservas de BiblioTech usando exclusivamente synchronized, wait y notifyAll (nada de java.util.concurrent). Debe tener:
- Capacidad máxima fija (por ejemplo 5).
poner(Reserva r): si está llena, espera hasta que haya sitio.tomar(): si está vacía, espera hasta que haya algo.- El bucle
whilecorrecto en los dos métodos, ynotifyAlltras cada modificación. - Soporte de interrupción: propagar
InterruptedException.
Escribe un main con dos productores y tres consumidores que demuestre que la cola nunca supera su capacidad y que nadie obtiene null.
Ejercicio 3: Provocar y diagnosticar un interbloqueo
Escribe InterbloqueoDiagnosticado que:
- Provoque deliberadamente un interbloqueo entre dos hilos llamados
bibliotech-prestamosybibliotech-devoluciones, que toman los candadosCATALOGOyREGISTROen orden opuesto. - Lance un tercer hilo demonio,
bibliotech-detector, que useThreadMXBean.findDeadlockedThreads()para detectarlo, imprimir un informe con los nombres de los hilos, los candados implicados y quién posee cada uno, y luego termine el programa conSystem.exit(1). - Documente en un comentario cómo obtendrías el mismo diagnóstico con
jstacky qué línea buscarías.
Después, escribe una segunda versión, SinInterbloqueo, que resuelva el problema imponiendo un orden global de adquisición de los candados, y comprueba que el detector ya no encuentra nada.
Soluciones
Solución al Ejercicio 1
import java.util.concurrent.TimeUnit;
public class ObservadorEstados {
private static final Object candado = new Object();
private static final Object senal = new Object();
public static void main(String[] args) throws InterruptedException {
final long inicio = System.nanoTime();
Thread observado = new Thread(() -> {
try {
// 1) TIMED_WAITING
TimeUnit.MILLISECONDS.sleep(400);
// 2) BLOCKED: main tiene 'candado' tomado
synchronized (candado) {
// (dentro apenas hacemos nada)
}
// 3) WAITING: espera indefinida hasta que main senalice
synchronized (senal) {
senal.wait();
}
// 4) TERMINATED al salir de run()
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "hilo-bajo-observacion");
Thread observador = new Thread(() -> {
Thread.State anterior = null;
try {
while (true) {
Thread.State actual = observado.getState();
// Solo imprimimos los CAMBIOS: si no, la salida
// seria ilegible con cientos de lineas repetidas.
if (actual != anterior) {
long ms = (System.nanoTime() - inicio) / 1_000_000;
System.out.printf("%5d ms %-16s%n", ms, actual);
anterior = actual;
}
if (actual == Thread.State.TERMINATED) {
return;
}
TimeUnit.MILLISECONDS.sleep(50);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "observador");
// Demonio: es informativo, no debe impedir el cierre de la JVM.
observador.setDaemon(true);
observador.start();
// Estado NEW visible durante 200 ms antes de arrancar.
TimeUnit.MILLISECONDS.sleep(200);
observado.start();
// A los 600 ms tomamos el candado 500 ms: el observado, que
// llega sobre los 400 ms, quedara BLOCKED.
TimeUnit.MILLISECONDS.sleep(400);
synchronized (candado) {
TimeUnit.MILLISECONDS.sleep(500);
}
// Le dejamos entrar en wait() y luego lo despertamos.
TimeUnit.MILLISECONDS.sleep(400);
synchronized (senal) {
senal.notifyAll();
}
observado.join();
TimeUnit.MILLISECONDS.sleep(200); // margen para la ultima linea
}
}Salida:
0 ms NEW
201 ms RUNNABLE
252 ms TIMED_WAITING
604 ms BLOCKED
1104 ms RUNNABLE
1155 ms WAITING
1508 ms TERMINATEDLos seis estados en orden, con sus tiempos. Fíjate en el paso de BLOCKED a WAITING: pasa por RUNNABLE a los 1104 ms, exactamente como decía el diagrama del apartado 2. No hay transiciones directas entre estados de espera.
Solución al Ejercicio 2
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.TimeUnit;
/**
* Cola acotada de reservas implementada con synchronized/wait/notifyAll.
*
* NOTA DIDACTICA: en produccion usarias una ArrayBlockingQueue (08-06),
* que hace exactamente esto mejor y sin margen de error. Esta clase existe
* para ver el mecanismo por dentro.
*/
public class ColaReservasCoordinada {
private final Deque<String> cola = new ArrayDeque<>();
private final int capacidad;
public ColaReservasCoordinada(int capacidad) {
if (capacidad <= 0) {
throw new IllegalArgumentException("capacidad debe ser positiva");
}
this.capacidad = capacidad;
}
/**
* Anade una reserva. Si la cola esta llena, espera.
* Propaga InterruptedException: quien llama decide (08-02).
*/
public void poner(String reserva) throws InterruptedException {
synchronized (cola) {
// WHILE, no if: al despertar hay que RE-COMPROBAR, porque
// otro productor pudo llenar el hueco antes que nosotros.
while (cola.size() == capacidad) {
cola.wait();
}
cola.addLast(reserva);
// notifyAll y no notify: en esta cola esperan hilos por DOS
// condiciones distintas (llena y vacia). Con notify podriamos
// despertar a un productor cuando el que puede avanzar es
// un consumidor, y perder la senal (apartado 9).
cola.notifyAll();
}
}
/** Extrae una reserva. Si la cola esta vacia, espera. Nunca devuelve null. */
public String tomar() throws InterruptedException {
synchronized (cola) {
while (cola.isEmpty()) {
cola.wait();
}
String r = cola.pollFirst();
cola.notifyAll();
return r;
}
}
public int tamano() {
synchronized (cola) {
return cola.size();
}
}
// --- Demostracion ---
public static void main(String[] args) throws InterruptedException {
final int CAPACIDAD = 5;
final int POR_PRODUCTOR = 10;
ColaReservasCoordinada cola = new ColaReservasCoordinada(CAPACIDAD);
Runnable productor = () -> {
String yo = Thread.currentThread().getName();
try {
for (int i = 1; i <= POR_PRODUCTOR; i++) {
cola.poner(yo + "-reserva-" + i);
System.out.printf(" [+] %-14s pone %2d (tamano=%d)%n",
yo, i, cola.tamano());
TimeUnit.MILLISECONDS.sleep(30);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
Runnable consumidor = () -> {
String yo = Thread.currentThread().getName();
try {
while (!Thread.currentThread().isInterrupted()) {
String r = cola.tomar();
// Sin 'while' en tomar(), esta comprobacion fallaria.
if (r == null) {
throw new IllegalStateException("null: bug de coordinacion");
}
System.out.printf(" [-] %-14s toma %-28s (tamano=%d)%n",
yo, r, cola.tamano());
TimeUnit.MILLISECONDS.sleep(70);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
Thread p1 = new Thread(productor, "productor-1");
Thread p2 = new Thread(productor, "productor-2");
Thread c1 = new Thread(consumidor, "consumidor-1");
Thread c2 = new Thread(consumidor, "consumidor-2");
Thread c3 = new Thread(consumidor, "consumidor-3");
c1.start(); c2.start(); c3.start();
p1.start(); p2.start();
p1.join();
p2.join();
// Dejamos que los consumidores vacien y los paramos con interrupt.
TimeUnit.MILLISECONDS.sleep(1000);
c1.interrupt(); c2.interrupt(); c3.interrupt();
c1.join(); c2.join(); c3.join();
System.out.println("Reservas pendientes al final: " + cola.tamano());
}
}Salida (fragmento):
[+] productor-1 pone 1 (tamano=1)
[+] productor-2 pone 1 (tamano=1)
[-] consumidor-1 toma productor-1-reserva-1 (tamano=1)
[-] consumidor-2 toma productor-2-reserva-1 (tamano=0)
[+] productor-1 pone 2 (tamano=1)
...
Reservas pendientes al final: 0Tres puntos didácticos:
- El tamaño nunca supera 5. La condición
while (cola.size() == capacidad)enponerlo garantiza. Si los productores fueran mucho más rápidos, se bloquearían en lugar de hacer crecer la cola sin límite: eso es contrapresión, y es una propiedad valiosísima que se retoma conBlockingQueueen 08-06. - Ningún consumidor obtiene
null. Elwhileentomarlo garantiza. Cambia esewhilepor unify ejecuta: con tres consumidores ynotifyAll, laIllegalStateExceptionsalta en segundos. - El tamaño impreso puede no cuadrar con la operación.
System.out.printfse ejecuta fuera del bloque sincronizado, así que entre poner y consultar el tamaño otro hilo pudo actuar. No es un bug de la cola: es la demostración de que una lectura fuera de la sección crítica es solo una foto de un instante ya pasado. Es exactamente el problema de "comprobar-luego-actuar" de 08-06.
Solución al Ejercicio 3
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.TimeUnit;
/**
* Provoca un interbloqueo y lo diagnostica desde dentro del programa.
*
* DIAGNOSTICO EXTERNO EQUIVALENTE:
* 1) jps -l -> localizar el pid
* 2) jstack <pid> > volcado.txt -> volcar los hilos
* 3) buscar en volcado.txt la linea "Found one Java-level deadlock:"
* Debajo aparecen los dos hilos, el candado que espera cada uno
* y quien lo posee, mas las trazas con "- locked" y
* "- waiting to lock" del mismo par de direcciones en orden inverso.
*/
public class InterbloqueoDiagnosticado {
// Dos candados que representan dos recursos del dominio.
private static final Object CATALOGO = new Object();
private static final Object REGISTRO = new Object();
public static void main(String[] args) {
arrancarDetector();
// Hilo A: CATALOGO -> REGISTRO
new Thread(() -> {
synchronized (CATALOGO) {
dormir(300); // deja tiempo al otro hilo
synchronized (REGISTRO) { // se queda BLOCKED aqui
System.out.println("A completado (nunca ocurre)");
}
}
}, "bibliotech-prestamos").start();
// Hilo B: REGISTRO -> CATALOGO <-- ORDEN INVERSO: la causa raiz
new Thread(() -> {
synchronized (REGISTRO) {
dormir(300);
synchronized (CATALOGO) { // se queda BLOCKED aqui
System.out.println("B completado (nunca ocurre)");
}
}
}, "bibliotech-devoluciones").start();
}
/** Vigilante demonio que detecta interbloqueos y aborta con informe. */
private static void arrancarDetector() {
Thread detector = new Thread(() -> {
ThreadMXBean mx = ManagementFactory.getThreadMXBean();
while (!Thread.currentThread().isInterrupted()) {
long[] ids = mx.findDeadlockedThreads(); // null si no hay
if (ids != null) {
System.err.println();
System.err.println("========================================");
System.err.println(" INTERBLOQUEO DETECTADO (" + ids.length + " hilos)");
System.err.println("========================================");
// true, true: incluir monitores y sincronizadores poseidos
for (ThreadInfo info : mx.getThreadInfo(ids, true, true)) {
System.err.printf("%nHilo : %s (%s)%n",
info.getThreadName(), info.getThreadState());
System.err.printf("Espera : %s%n", info.getLockName());
System.err.printf("Poseido por: %s%n", info.getLockOwnerName());
System.err.println("Traza (2 primeras lineas):");
StackTraceElement[] traza = info.getStackTrace();
for (int i = 0; i < Math.min(2, traza.length); i++) {
System.err.println(" at " + traza[i]);
}
}
System.err.println();
System.err.println("CAUSA: los dos hilos adquieren los mismos dos");
System.err.println("candados en ORDEN OPUESTO. Solucion en 08-04:");
System.err.println("imponer un orden global de adquisicion.");
System.exit(1);
}
dormir(500);
}
}, "bibliotech-detector");
detector.setDaemon(true); // no debe impedir el cierre
detector.start();
}
private static void dormir(long ms) {
try {
TimeUnit.MILLISECONDS.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Salida:
========================================
INTERBLOQUEO DETECTADO (2 hilos)
========================================
Hilo : bibliotech-prestamos (BLOCKED)
Espera : java.lang.Object@5b6f7412
Poseido por: bibliotech-devoluciones
Traza (2 primeras lineas):
at InterbloqueoDiagnosticado.lambda$main$0(InterbloqueoDiagnosticado.java:31)
at java.base/java.lang.Thread.run(Thread.java:1583)
Hilo : bibliotech-devoluciones (BLOCKED)
Espera : java.lang.Object@2d1ef81a
Poseido por: bibliotech-prestamos
Traza (2 primeras lineas):
at InterbloqueoDiagnosticado.lambda$main$1(InterbloqueoDiagnosticado.java:42)
at java.base/java.lang.Thread.run(Thread.java:1583)
CAUSA: los dos hilos adquieren los mismos dos candados en ORDEN OPUESTO.
Solucion en 08-04: imponer un orden global de adquisicion.Y la versión corregida, con orden global de adquisición:
/**
* Misma funcionalidad, sin interbloqueo posible.
*
* REGLA: TODOS los hilos adquieren los candados en el MISMO orden,
* siempre CATALOGO antes que REGISTRO. Con un orden total sobre los
* candados no puede formarse un ciclo de espera, y sin ciclo no hay
* interbloqueo. Es la solucion que se desarrolla en 08-04.
*/
public class SinInterbloqueo {
private static final Object CATALOGO = new Object();
private static final Object REGISTRO = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable prestar = () -> {
synchronized (CATALOGO) { // 1º CATALOGO
dormir(300);
synchronized (REGISTRO) { // 2º REGISTRO
System.out.println("[" + Thread.currentThread().getName()
+ "] prestamo registrado");
}
}
};
Runnable devolver = () -> {
synchronized (CATALOGO) { // 1º CATALOGO (antes era REGISTRO)
dormir(300);
synchronized (REGISTRO) { // 2º REGISTRO
System.out.println("[" + Thread.currentThread().getName()
+ "] devolucion registrada");
}
}
};
Thread a = new Thread(prestar, "bibliotech-prestamos");
Thread b = new Thread(devolver, "bibliotech-devoluciones");
a.start(); b.start();
a.join(); b.join();
System.out.println("Ambos hilos han terminado: NO hay interbloqueo");
}
private static void dormir(long ms) {
try { TimeUnit.MILLISECONDS.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}Salida:
[bibliotech-prestamos] prestamo registrado
[bibliotech-devoluciones] devolucion registrada
Ambos hilos han terminado: NO hay interbloqueoEl cambio es de una sola línea —invertir el orden de los dos synchronized en devolver— y elimina por completo una clase de fallo que puede tumbar una aplicación en producción sin dejar rastro. Es la mejor relación coste/beneficio de todo el módulo, y en 08-04 se convierte en una regla de diseño explícita.
Conclusión
Ya no ves un hilo como una caja negra que arranca y acaba: sabes exactamente en qué puede estar y qué lo mueve de un sitio a otro.
Los seis estados de Thread.State y sus transiciones: NEW antes de start(), RUNNABLE cuando está ejecutando o listo para ejecutar, BLOCKED esperando un monitor, WAITING esperando una señal, TIMED_WAITING esperando con plazo, y TERMINATED como estado terminal e irreversible —que es la razón profunda de que un Thread no se pueda reutilizar y de que existan los pools—. Con la observación estructural que ordena el diagrama: todas las transiciones pasan por RUNNABLE; un hilo que despierta de wait() tiene que readquirir el monitor, y en ese momento puede quedar BLOCKED.
Conoces los dos matices que separan a quien sabe leer un sistema en producción de quien no. El primero: RUNNABLE no significa "consumiendo CPU", porque agrupa ejecutar, esperar turno y —esto sorprende siempre— estar bloqueado en E/S clásica; hay que mirar la traza de pila y el campo cpu=, no el estado. El segundo: BLOCKED es "no me dejan entrar" y WAITING es "espero a que me avisen", contención frente a coordinación, y esa distinción convierte un volcado ilegible en un diagnóstico.
Dominas el mecanismo de coordinación de bajo nivel: wait, notify y notifyAll viven en Object, se llaman siempre con el monitor en la mano —o IllegalMonitorStateException—, y wait() suelta el monitor mientras que sleep() no, que es la diferencia que define para qué sirve cada uno. Y tienes la regla que más se incumple: wait() va siempre dentro de un while, nunca dentro de un if, por tres motivos independientes —despertares espurios, notifyAll con varios esperando y un solo elemento, y condiciones que cambian entre la señal y el despertar—, con la idea que los resume: notify no dice "tu condición se cumple", dice "vuelve a mirar". Y la recomendación derivada: notifyAll por defecto, porque notify con hilos esperando por condiciones distintas produce señales perdidas y bloqueos silenciosos.
Sabes por qué stop, suspend y resume llevan retirados desde 1998: el primero suelta todos los monitores en mitad de una operación y deja estructuras corruptas sin dar ningún error; los otros dos retienen los monitores mientras el hilo está pausado, lo que produce interbloqueos perfectos. La alternativa es la misma idea en los dos casos: el hilo elige dónde se detiene, que es la cancelación cooperativa de 08-02 y el punto de pausa explícito con wait/notify.
Y, sobre todo, sabes diagnosticar. Obtener un volcado con Ctrl+\, jstack o jcmd; leer una entrada campo a campo —nombre, estado, cpu=, - locked y - waiting to lock, traza—; reconocer los cuatro perfiles habituales —trabajando, esperando E/S, hilo de pool ocioso, bloqueado—; y encontrar el Found one Java-level deadlock que la JVM escribe por ti, con el ciclo de espera y las líneas exactas del código. Con la regla operativa que evita conclusiones falsas: tres volcados separados por unos segundos, porque un hilo puede estar de paso pero no puede estar de paso tres veces en la misma línea. Y con Thread.getAllStackTraces y ThreadMXBean.findDeadlockedThreads() para diagnosticar desde dentro, más jconsole, VisualVM y JFR para hacerlo con interfaz gráfica.
BiblioTech ya se puede observar. MonitorHilos muestra el estado de los hilos de importación y de avisos mientras trabajan, y esa salida enseña algo que hasta ahora era teoría: el importador alterna RUNNABLE y TIMED_WAITING —tarea mixta—, mientras que el hilo de avisos está casi siempre en TIMED_WAITING —tarea limitada por E/S—. Es la confirmación empírica de la tabla de 08-01 y la justificación de todo lo que viene.
Porque ahora ya sabes observar el problema, y llega el momento de resolverlo. Has visto un interbloqueo real, has visto tres consumidores obtener null por un if mal puesto, y sigues teniendo pendiente el contador que perdía medio millón de incrementos desde 08-01.
En la próxima lección, Sincronización —la lección central del módulo—, se resuelve todo eso. Verás por qué contador++ son en realidad tres operaciones y qué entrelazado exacto pierde los incrementos; el modelo de memoria de Java, con las cachés por núcleo, la reordenación de instrucciones y la relación happens-before que lo gobierna todo; qué garantiza volatile —visibilidad— y qué no —atomicidad—, con los dos ejemplos que lo demuestran; synchronized en todas sus formas, el monitor intrínseco, el bloqueo privado y la granularidad correcta; el interbloqueo reproducible y sus dos soluciones —orden global y tryLock con plazo—; ReentrantLock con sus Condition, ReadWriteLock para estructuras que se leen mucho y se escriben poco, y la mejor estrategia de todas: no compartir. Al terminar, el Catalogo y el RegistroPrestamos de BiblioTech serán seguros para varios hilos, y el caso C de 08-01 —dos empleados trabajando a la vez— dejará de ser una amenaza.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
