A la lliçó anterior vas arrencar fils amb start(), els vas esperar amb join() i els vas cancel·lar amb interrupt(). Pel camí van aparèixer dos valors de getState() —NEW i TERMINATED— sense explicar què són ni quants n'hi ha més.
Aquesta lliçó els explica tots. Un fil de Java està sempre en exactament un de sis estats, i saber en quin està i per què és la diferència entre diagnosticar un problema de producció en cinc minuts o en dos dies. Quan una aplicació "es penja", la pregunta no és "per què no funciona?" sinó "en quin estat són els seus fils i què estan esperant?". Aquesta pregunta es respon amb un bolcat de fils, i llegir-lo és una habilitat que val per si sola el preu d'aquesta lliçó.
Veuràs també el mecanisme de coordinació de més baix nivell que existeix en Java —wait, notify i notifyAll—, que és el que hi ha sota de totes les utilitats còmodes de 08-05 i 08-06; per què cal embolcallar-lo sempre en un bucle while i per què un if allà és un error real, no una preferència d'estil; i per què tres mètodes de Thread que semblaven molt útils —stop, suspend i resume— fa més de vint anys que estan marcats com a perillosos.
Advertiment de nivell.
wait/notifyés l'única part d'aquest mòdul que probablement no escriuràs mai en producció:BlockingQueueiCountDownLatchho fan millor i sense errors. S'estudia perquè explica com funcionen aquestes eines i perquè apareix constantment en llegir codi aliè i bolcats de fils.
Contingut
- Els sis estats de
Thread.State - El diagrama complet del cicle de vida
NEWiTERMINATED: els extremsRUNNABLE: el matís que confon tothomBLOCKEDdavant deWAITING: la distinció que fa llegible un bolcatTIMED_WAITING: esperar amb terminiwait,notifyinotifyAll- El bucle
whileobligatori notifydavant denotifyAllyieldionSpinWait- Els mètodes retirats:
stop,suspend,resume - Diagnòstic a la pràctica: el bolcat de fils
- Llegir un interbloqueig detectat per la JVM
- Inspecció des del propi programa
- BiblioTech: seguir l'estat dels seus fils
- Errors Comuns i Consells
- Exercicis
- Els sis estats de
Thread.State
Thread.StateThread.State és un enum —dels de 04-07— amb sis constants. Un fil està sempre en una i només una d'elles.
| Estat | Significat | Com s'hi arriba | Com se'n surt |
|---|---|---|---|
NEW |
Objecte Thread creat, encara no arrencat |
new Thread(...) |
start() |
RUNNABLE |
Executant, o llest per executar i esperant nucli | start(); tornar d'una espera |
Acabar run(); bloquejar-se; esperar |
BLOCKED |
Esperant adquirir el monitor d'un objecte | Entrar en synchronized ocupat |
El monitor queda lliure i s'adquireix |
WAITING |
Esperant indefinidament un senyal d'un altre fil | wait(), join(), LockSupport.park() |
notify/notifyAll, fi del fil esperat, interrupció |
TIMED_WAITING |
Igual que WAITING però amb termini |
sleep(ms), wait(ms), join(ms), parkNanos |
Senyal, termini exhaurit o interrupció |
TERMINATED |
run() ha acabat (bé o amb excepció) |
Retorn o excepció no capturada de run() |
Mai: és terminal |
Dos fets que convé fixar des del principi:
TERMINATEDés irreversible. Un fil acabat no es pot reiniciar.start()sobre ell llançaIllegalThreadStateException. Aquesta és la raó profunda per la qual existeixen els pools: si el fil no es pot reutilitzar, cal reutilitzar el fil, no l'objecte — és a dir, mantenir-lo viu donant-li tasques noves.Thread.Stateés una vista de la JVM, no del sistema operatiu. L'estat que veus és el que la JVM sap; el planificador del sistema té la seva pròpia idea, més fina.
- El diagrama complet del cicle de vida
stateDiagram-v2
[*] --> NEW : new Thread(tasca)
NEW --> RUNNABLE : start()
RUNNABLE --> BLOCKED : entra en synchronized ocupat
BLOCKED --> RUNNABLE : obté el monitor
RUNNABLE --> WAITING : wait()
RUNNABLE --> WAITING : join() sense termini
RUNNABLE --> WAITING : LockSupport.park()
WAITING --> RUNNABLE : notify o notifyAll
WAITING --> RUNNABLE : acaba el fil esperat
WAITING --> RUNNABLE : interrupt()
RUNNABLE --> TIMED_WAITING : sleep(ms)
RUNNABLE --> TIMED_WAITING : wait(ms)
RUNNABLE --> TIMED_WAITING : join(ms)
TIMED_WAITING --> RUNNABLE : termini exhaurit
TIMED_WAITING --> RUNNABLE : notify o interrupt
RUNNABLE --> TERMINATED : run() retorna
RUNNABLE --> TERMINATED : excepcio no capturada
TERMINATED --> [*]
Dues observacions estructurals sobre aquest diagrama:
Primera: tot passa per RUNNABLE. No hi ha cap transició directa entre BLOCKED, WAITING i TIMED_WAITING. Un fil que està esperant un senyal i després necessita un monitor passa per RUNNABLE enmig. Això importa en llegir bolcats: un fil que surt de wait() ha de readquirir el monitor abans de continuar, i en aquell instant pot quedar BLOCKED.
Segona: només hi ha una sortida de TERMINATED, i és cap a fora. No hi ha fletxa de tornada. És el punt que fa que "reutilitzar un fil" sigui impossible per disseny.
Aquest petit programa recorre gairebé tots els estats:
import java.util.concurrent.TimeUnit;
public class RecorregutEstats {
private static final Object pany = new Object();
public static void main(String[] args) throws InterruptedException {
Thread observat = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300); // TIMED_WAITING
synchronized (pany) { // BLOCKED (si esta ocupat)
pany.wait(500); // TIMED_WAITING
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "fil-observat");
System.out.println("Abans de start() : " + observat.getState()); // NEW
observat.start();
System.out.println("Just despres start() : " + observat.getState()); // RUNNABLE
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Dormint : " + observat.getState()); // TIMED_WAITING
// Prenem el pany perque el fil observat quedi BLOCKED en demanar-lo.
synchronized (pany) {
TimeUnit.MILLISECONDS.sleep(400);
System.out.println("Pany ocupat : " + observat.getState()); // BLOCKED
}
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Dins de wait(500) : " + observat.getState()); // TIMED_WAITING
observat.join();
System.out.println("Despres de join() : " + observat.getState()); // TERMINATED
}
}Sortida:
Abans de start() : NEW
Just despres start() : RUNNABLE
Dormint : TIMED_WAITING
Pany ocupat : BLOCKED
Dins de wait(500) : TIMED_WAITING
Despres de join() : TERMINATEDAquest programa és intrínsecament fràgil i és honest dir-ho: depèn que els
sleepde l'observador caiguin en els moments justos. En una màquina carregada podria imprimir una altra cosa. Serveix per veure els estats, no com a tècnica de diagnòstic: per a això hi ha els bolcats de l'apartat 12.
NEW i TERMINATED: els extrems
NEW i TERMINATED: els extremsNEW és l'estat de l'objecte Thread acabat de construir. El fil del sistema operatiu encara no existeix: new Thread(...) només reserva un objecte Java. El fil natiu es crea a start().
D'aquí surt el diagnòstic de l'error de 08-02: si un fil apareix com a NEW quan esperaves que estigués treballant, és que ningú no va cridar start() —o que es va cridar run() per error—.
TERMINATED és l'estat després del fi de run(), per qualsevol de les seves dues vies:
public class DuesFormesAcabar {
public static void main(String[] args) throws InterruptedException {
Thread be = new Thread(() -> System.out.println("[be] feina feta"), "be");
Thread mal = new Thread(() -> { throw new RuntimeException("fallada"); }, "mal");
be.start(); be.join();
mal.start(); mal.join();
System.out.println("be -> " + be.getState()); // TERMINATED
System.out.println("mal -> " + mal.getState()); // TERMINATED
}
}Tots dos acaben en TERMINATED. L'estat no distingeix l'èxit de la fallada, i aquesta és exactament la trampa de 08-02: join() retorna amb normalitat tant si el fil va fer la seva feina com si va morir per una excepció. Per distingir-los necessites o bé un gestor d'excepcions no capturades, o bé un Future (08-05), que sí que desa l'excepció i te la rellança a get().
RUNNABLE: el matís que confon tothom
RUNNABLE: el matís que confon tothomMolts manuals dibuixen un estat RUNNING separat de RUNNABLE. Java no el té. Thread.State.RUNNABLE agrupa dues situacions molt diferents:
- Executant ara mateix en un nucli.
- Llest per executar, a la cua del planificador, esperant que li donin un nucli.
La JVM no les distingeix perquè no pot: qui executa en cada instant ho decideix el planificador del sistema operatiu, sense informar la JVM. Amb vuit fils RUNNABLE en una màquina de quatre nuclis, en qualsevol instant n'hi ha com a molt quatre executant de veritat i almenys quatre esperant torn, i tots apareixen com a RUNNABLE.
Hi ha una tercera situació, encara més contraintuïtiva:
Un fil bloquejat en E/S clàssica apareix com a
RUNNABLE.
Un fil aturat a InputStream.read() esperant el disc fa una estona que no executa ni una instrucció, però getState() diu RUNNABLE. La raó és que la JVM no té ni idea que aquell fil està aparcat dins d'una crida al sistema: des del seu punt de vista, el fil està executant codi natiu.
// Aquest fil pot passar 30 segons aqui i el seu estat sera RUNNABLE
// tota l'estona, encara que no consumeixi ni un cicle de CPU.
Thread lector = new Thread(() -> {
try (BufferedReader br = Files.newBufferedReader(Path.of("dades/enorme.csv"))) {
String linia;
while ((linia = br.readLine()) != null) { processar(linia); }
} catch (IOException e) { /* ... */ }
}, "bibliotech-lector");Conseqüència pràctica per diagnosticar: veure molts fils RUNNABLE no significa que la CPU estigui saturada. Cal mirar la traça de pila:
- Si el cim és
java.net.SocketInputStream.socketRead0oFileInputStream.readBytes, aquell fil està esperant E/S, no calculant. - Si el cim és codi teu dins d'un bucle, llavors sí que està consumint CPU.
Aquesta distinció és la que separa "necessito més nuclis" de "necessito més fils perquè estic esperant el disc" —la taula de tasques limitades per CPU i per E/S de 08-01, vista ara des de l'altre costat—.
BLOCKED davant de WAITING: la distinció que fa llegible un bolcat
BLOCKED davant de WAITING: la distinció que fa llegible un bolcatAquesta és la distinció més valuosa de la lliçó per a la feina real.
BLOCKED |
WAITING |
|
|---|---|---|
| Què espera | Un monitor (la clau d'un synchronized) |
Un senyal d'un altre fil |
| Com s'hi arriba | Intentar entrar en un synchronized ocupat |
wait(), join(), LockSupport.park() |
| Com se'n surt | L'amo del monitor el deixa anar | Un altre fil crida notify/notifyAll, acaba el fil esperat, o arriba una interrupció |
| Depèn d'un altre fil? | Sí, del que té el monitor | Sí, del que ha de senyalitzar |
| En un bolcat | waiting to lock <0x...> |
in Object.wait() / parking to wait for <0x...> |
| Què sol indicar | Contenció: molts fils barallant-se per un pany | Coordinació: cua buida, tasca pendent, join |
| Si dura molt | Sospita d'interbloqueig o secció crítica llarga | Normal en un pool ociós; sospitós si ningú no senyalitzarà |
La forma curta de recordar-ho:
BLOCKED= "no em deixen entrar".WAITING= "estic esperant que m'avisin".
Demostració dels dos estats alhora:
import java.util.concurrent.TimeUnit;
public class BloquejatVsEsperant {
private static final Object pany = new Object();
public static void main(String[] args) throws InterruptedException {
// Fil 1: pren el pany i es queda dins fent wait().
// Deixa anar el monitor en entrar a wait(), pero queda WAITING.
Thread esperador = new Thread(() -> {
synchronized (pany) {
try {
pany.wait(); // WAITING (i DEIXA ANAR el monitor)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "fil-esperador");
// Fil 2: pren el pany i el rete dormint.
// sleep NO deixa anar el monitor (08-02, apartat 8).
Thread acaparador = new Thread(() -> {
synchronized (pany) {
try {
TimeUnit.SECONDS.sleep(5); // TIMED_WAITING, amb el monitor
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "fil-acaparador");
// Fil 3: vol el pany i no l'aconsegueix -> BLOCKED.
Thread bloquejat = new Thread(() -> {
synchronized (pany) {
System.out.println("[bloquejat] per fi he entrat");
}
}, "fil-bloquejat");
esperador.start();
TimeUnit.MILLISECONDS.sleep(200); // que arribi a wait()
acaparador.start();
TimeUnit.MILLISECONDS.sleep(200); // que prengui el pany
bloquejat.start();
TimeUnit.MILLISECONDS.sleep(200); // que xoqui amb el pany
System.out.println("esperador : " + esperador.getState());
System.out.println("acaparador : " + acaparador.getState());
System.out.println("bloquejat : " + bloquejat.getState());
// Neteja
acaparador.interrupt();
synchronized (pany) { pany.notifyAll(); }
}
}Sortida:
Els tres estats alhora, i cadascun per un motiu diferent. Fixa't en el detall crucial d'esperador: va entrar al synchronized, o sigui, tenia el monitor, i en cridar wait() el va deixar anar. Per això acaparador va poder entrar després. Aquesta és la diferència essencial entre wait() i sleep():
Thread.sleep(ms) |
objecte.wait() |
|
|---|---|---|
| Deixa anar el monitor? | No | Sí |
| On es pot cridar | A qualsevol lloc | Només dins de synchronized sobre aquell objecte |
| Com es desperta | Termini exhaurit o interrupció | notify/notifyAll, termini, interrupció o espuri |
| Estat resultant | TIMED_WAITING |
WAITING o TIMED_WAITING |
| Per a què serveix | Pausar | Coordinar |
TIMED_WAITING: esperar amb termini
TIMED_WAITING: esperar amb terminiTIMED_WAITING és la versió amb termini de WAITING. Els mètodes que hi porten:
Thread.sleep(1000); // TIMED_WAITING
TimeUnit.SECONDS.sleep(1); // idem
objecte.wait(1000); // TIMED_WAITING, deixes anar el monitor
fil.join(1000); // TIMED_WAITING
LockSupport.parkNanos(1_000_000_000L); // TIMED_WAITING
bloqueig.tryLock(1, TimeUnit.SECONDS); // TIMED_WAITING (08-04)
cua.poll(1, TimeUnit.SECONDS); // TIMED_WAITING (08-06)En un bolcat, TIMED_WAITING (sleeping) i TIMED_WAITING (on object monitor) es distingeixen entre si, cosa que és útil: el primer és un fil que va decidir pausar-se, el segon és un fil esperant un senyal amb termini.
El detall de diagnòstic que estalvia hores: un fil permanentment en TIMED_WAITING (sleeping) dins d'un bucle és gairebé sempre un sondeig (polling), i gairebé sempre és un disseny millorable:
// ANTIPATRO: sondeig. El fil es desperta 10 vegades per segon
// nomes per comprovar si hi ha alguna cosa, i normalment no n'hi ha.
while (cuaReserves.isEmpty()) {
Thread.sleep(100);
}
Reserva r = cuaReserves.poll();
// CORRECTE: bloquejar-se fins que hi hagi alguna cosa, sense despertar en va (08-06).
Reserva r = cuaReserves.take(); // BlockingQueue
wait, notify i notifyAll
wait, notify i notifyAllAquests tres mètodes no són a Thread: són a Object, la classe de 03-09. Qualsevol objecte de Java pot servir de punt de coordinació, perquè qualsevol objecte té un monitor associat.
| Mètode | Què fa |
|---|---|
objecte.wait() |
Deixa anar el monitor d'objecte i deixa el fil en WAITING fins a rebre un senyal |
objecte.wait(ms) |
Igual, però es desperta també en exhaurir-se el termini (TIMED_WAITING) |
objecte.notify() |
Desperta un fil (arbitrari) dels que esperen en objecte |
objecte.notifyAll() |
Desperta tots els fils que esperen en objecte |
La regla número u, que el compilador no comprova:
Els tres mètodes s'han de cridar amb el monitor d'aquell objecte a la mà, és a dir, dins d'un bloc o mètode
synchronizedsobre aquell mateix objecte. Si no, es llançaIllegalMonitorStateExceptionen temps d'execució.
Object pany = new Object();
// MALAMENT: sense el monitor -> IllegalMonitorStateException
pany.wait();
// BE
synchronized (pany) {
pany.wait();
}Per què existeix aquesta regla —i és una pregunta d'entrevista clàssica—: sense ella hi hauria una condició de cursa irreparable. Imagina que poguessis fer wait() sense el monitor:
- El consumidor comprova
cua.isEmpty()→true. - Justament aquí el productor afegeix un element i crida
notify(). - El consumidor crida
wait()… i es perd el senyal, perquè va arribar abans que comencés a esperar.
Resultat: el consumidor espera per sempre amb la cua plena. Exigir el monitor fa que "comprovar la condició" i "començar a esperar" siguin una sola operació indivisible des del punt de vista del productor, perquè aquest necessita el mateix monitor per senyalitzar.
Exemple mínim de coordinació:
import java.util.ArrayDeque;
import java.util.Deque;
public class CoordinacioBasica {
// L'objecte de bloqueig I de senyalitzacio: la propia cua.
private static final Deque<String> reserves = new ArrayDeque<>();
public static void main(String[] args) throws InterruptedException {
Thread consumidor = new Thread(() -> {
try {
synchronized (reserves) {
// BUCLE while, no if. Apartat 8.
while (reserves.isEmpty()) {
System.out.println("[consumidor] cua buida, espero");
reserves.wait(); // deixa anar el monitor i espera
}
System.out.println("[consumidor] atenc: " + reserves.poll());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "bibliotech-consumidor");
Thread productor = new Thread(() -> {
try {
Thread.sleep(500);
synchronized (reserves) {
reserves.add("Reserva de Marta Ruiz: 978-0000000001");
System.out.println("[productor] reserva afegida, aviso");
reserves.notifyAll(); // desperta els que esperen
} // el monitor s'allibera AQUI
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "bibliotech-productor");
consumidor.start();
productor.start();
consumidor.join();
productor.join();
}
}Sortida:
[consumidor] cua buida, espero
[productor] reserva afegida, aviso
[consumidor] atenc: Reserva de Marta Ruiz: 978-0000000001Un detall que es passa per alt: notifyAll() no deixa anar el monitor. El fil senyalitzador continua dins del synchronized fins a la clau de tancament. El consumidor es desperta, però no pot continuar fins que el monitor quedi lliure; mentrestant passa de WAITING a BLOCKED. És la transició de l'apartat 2 —tot passa per RUNNABLE— vista en acció, i la raó per la qual convé senyalitzar al final del bloc, no al principi.
Nota de precedència.
synchronizeds'estudia a fons a 08-04: què és exactament un monitor, la reentrada, el bloqueig privat i la granularitat. Aquí n'hi ha prou amb la regla operativa: per cridarwait,notifyonotifyAllsobre un objecte, cal estar dins d'unsynchronizedsobre aquell mateix objecte.
- El bucle
while obligatori
while obligatoriAquesta és la regla que més s'incompleix i la que produeix errors més difícils de reproduir:
wait()va SEMPRE dins d'un buclewhileque comprova la condició. Mai dins d'unif.
// MALAMENT - un bug real, no una questio d'estil
synchronized (reserves) {
if (reserves.isEmpty()) {
reserves.wait();
}
Reserva r = reserves.poll(); // pot ser null
atendre(r); // NullPointerException
}
// BE
synchronized (reserves) {
while (reserves.isEmpty()) {
reserves.wait();
}
Reserva r = reserves.poll(); // garantit no nul
atendre(r);
}Hi ha tres motius independents, i cadascun n'hi hauria prou per si sol:
Motiu 1: despertars espuris. L'especificació de Java permet explícitament que wait() retorni sense que ningú hagi cridat notify. No és una fallada d'implementació: és una concessió deliberada a com funcionen les primitives de sincronització dels sistemes operatius subjacents (pthread_cond_wait té el mateix advertiment). Són rars, però passen, i passen més en màquines carregades —o sigui, en producció i no al teu portàtil—.
Motiu 2: notifyAll desperta tothom, però només un pot guanyar. Amb tres consumidors esperant i un sol element a la cua, notifyAll() desperta els tres. El primer a readquirir el monitor s'emporta l'element; els altres dos tornen a comprovar i —amb while— tornen a esperar. Amb if, continuen endavant i es troben la cua buida.
Motiu 3: la condició pot haver canviat entre el senyal i el despertar. Entre notify() i el moment en què el fil despertat readquireix el monitor passa temps, i en aquest temps un altre fil pot haver entrat i consumit l'element. El senyal diu "alguna cosa ha canviat", no "la teva condició es compleix ara".
El tercer motiu és el més important i el que fa del while una necessitat lògica, no una precaució: notify no transmet informació sobre l'estat, només diu "torna a mirar". L'única font de veritat és la condició, i per això cal reavaluar-la.
Demostració de la fallada amb if:
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.TimeUnit;
public class ElBugDelIf {
private static final Deque<Integer> cua = new ArrayDeque<>();
static Runnable consumidorAmbIf = () -> {
try {
synchronized (cua) {
if (cua.isEmpty()) { // BUG: hauria de ser while
cua.wait();
}
Integer v = cua.poll();
System.out.println("[" + Thread.currentThread().getName()
+ "] consumeix: " + v
+ (v == null ? " <-- FALLADA: cua buida" : ""));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
public static void main(String[] args) throws InterruptedException {
// Tres consumidors esperant...
for (int i = 1; i <= 3; i++) {
new Thread(consumidorAmbIf, "consumidor-" + i).start();
}
TimeUnit.MILLISECONDS.sleep(300);
// ...i UN sol element amb notifyAll.
synchronized (cua) {
cua.add(42);
cua.notifyAll(); // desperta els TRES
}
TimeUnit.MILLISECONDS.sleep(500);
}
}Sortida:
[consumidor-1] consumeix: 42
[consumidor-2] consumeix: null <-- FALLADA: cua buida
[consumidor-3] consumeix: null <-- FALLADA: cua buidaDos dels tres consumidors obtenen null i en un programa real això seria un NullPointerException a la línia següent. Canvia if per while i executa de nou: els consumidors 2 i 3 tornen a esperar en silenci, que és exactament el correcte.
notify davant de notifyAll
notify davant de notifyAllnotify() |
notifyAll() |
|
|---|---|---|
| A quants desperta | A un, triat arbitràriament | A tots els que esperen en aquell objecte |
| Cost | Menor | Major: tots es desperten i competeixen pel monitor |
| Risc | Senyal perdut: pot despertar el fil equivocat | Cap de correcció; només ineficiència |
| Quan és segur | Tots els que esperen són intercanviables i esperen la mateixa condició | Sempre |
La recomanació és taxativa: fes servir notifyAll() llevat que puguis demostrar que és segur fer servir notify().
El perill de notify() és el despertar perdut. Si al mateix objecte hi esperen fils per condicions diferents —uns perquè la cua està buida, altres perquè està plena—, notify() pot despertar precisament un que no pot avançar. Aquell fil comprova la seva condició, continua sense complir-se, torna a wait()… i el senyal s'ha consumit. El fil que sí que podia avançar no va ser avisat mai, i el sistema es queda aturat sense cap error ni excepció.
// PERILLOS: dues condicions diferents esperant en el MATEIX objecte
synchronized (memoria) {
while (memoria.estaPlena()) memoria.wait(); // productors
// ...
memoria.notify(); // pot despertar UN ALTRE productor
}
synchronized (memoria) {
while (memoria.estaBuida()) memoria.wait(); // consumidors
// ...
memoria.notify(); // pot despertar UN ALTRE consumidor
}En aquest disseny, notify() pot acabar en un bloqueig total del sistema. Amb notifyAll() el problema desapareix: tots comproven, els que poden avancen, els que no tornen a esperar. Es paga una mica de cost per una garantia de correcció; gairebé sempre és el millor canvi possible.
La solució elegant —una espera per cada condició— existeix: són els objectes
ConditiondeReentrantLock, que permetensignal()sobre exactament el grup correcte de fils. Es veuen a 08-04.
yield i onSpinWait
yield i onSpinWaitDos mètodes menors, en una nota.
Thread.yield() és un suggeriment al planificador: "si hi ha un altre fil llest, dona-li pas". No garanteix res; el planificador el pot ignorar completament, i el fil pot tornar a ser triat immediatament. No canvia l'estat (continua RUNNABLE) i no deixa anar cap monitor. No el facis servir per a la correcció; el seu lloc legítim són les proves d'estrès, on forçar més canvis de context ajuda que apareguin les condicions de cursa latents.
// Us legitim: en una prova, per provocar entrellacats diferents
// i augmentar la probabilitat que un bug de cursa es manifesti.
for (int i = 0; i < 1000; i++) {
comptador.incrementar();
if (i % 10 == 0) Thread.yield();
}Thread.onSpinWait() (Java 9) és una pista per a la CPU dins d'una espera activa molt curta: emet una instrucció com PAUSE en x86, que redueix el consum energètic i millora el rendiment en sortir del bucle. Només té sentit en bucles d'espera de nanosegons i en codi de molt baix nivell:
// Espera activa MOLT curta, en codi d'alt rendiment.
// En codi d'aplicacio normal aixo es un antipatro:
// consumeix un nucli sencer. Fes servir wait/notify o una BlockingQueue.
while (!llest) {
Thread.onSpinWait();
}Cap dels dos no resol un problema de coordinació. Si creus que necessites yield() perquè el teu programa funcioni, el que necessites és sincronització.
- Els mètodes retirats:
stop, suspend, resume
stop, suspend, resumeThread té tres mètodes que feien exactament el que un voldria —aturar un fil, pausar-lo i reprendre'l— i que estan marcats com a obsolets des de Java 1.2 (1998). En Java 20 stop() va passar a llançar UnsupportedOperationException sempre, i suspend/resume van ser marcats per a eliminació. Val la pena entendre per què, perquè el motiu ensenya alguna cosa real.
Thread.stop(): matava el fil llançant-li un ThreadDeath al punt on fos.
El problema: deixa anar tots els seus monitors instantàniament, enmig del que estigués fent.
// Suposem que el fil es AQUI quan li arriba stop():
synchronized (cataleg) {
cataleg.eliminarIsbn(isbn); // ja s'ha executat
// <-- stop() cau justament aqui
cataleg.eliminarDelIndex(isbn); // NO s'executa MAI
}Resultat: l'ISBN es va treure del conjunt però no de l'índex. El catàleg queda incoherent, el monitor s'allibera i els altres fils entren tan tranquils a llegir una estructura corrupta. I no hi ha cap excepció ni traça que ho indiqui: la fallada apareix més tard, en un altre lloc, sense relació aparent.
És exactament el problema que resol la cancel·lació cooperativa de 08-02: interrupt() no interromp on li ve de gust, sinó als punts que el propi fil ha designat, on l'estat és coherent.
Thread.suspend() i resume(): pausaven i reprenien un fil.
El problema: suspend() no deixa anar els monitors. Un fil suspès dins d'un synchronized manté el pany indefinidament. Si el fil que anava a cridar resume() necessita aquell mateix pany per arribar-hi, interbloqueig perfecte, sense error ni traça.
// La recepta de l'interbloqueig:
filTreballador.suspend(); // suspes retenint el monitor de 'cataleg'
synchronized (cataleg) { // main queda BLOCKED aqui, per sempre
filTreballador.resume(); // aquesta linia no s'assoleix mai
}Què es fa servir al seu lloc:
| Mètode retirat | Substitut |
|---|---|
stop() |
interrupt() + protocol cooperatiu (08-02) |
suspend() / resume() |
Marcador volatile + wait/notify, o Semaphore/CyclicBarrier (08-05) |
destroy() |
No es va arribar a implementar mai; eliminat |
Pausa i represa ben fetes:
public class TascaPausable implements Runnable {
private final Object monitor = new Object();
private boolean pausada = false; // protegit per 'monitor'
private volatile boolean aturada = false;
public void pausar() { synchronized (monitor) { pausada = true; } }
public void reprendre() {
synchronized (monitor) {
pausada = false;
monitor.notifyAll(); // desperta el treballador
}
}
public void aturar() { aturada = true; }
@Override
public void run() {
try {
while (!aturada && !Thread.currentThread().isInterrupted()) {
// PUNT DE PAUSA triat pel propi fil: aqui l'estat
// es coherent i no es rete cap pany de negoci.
// Res a veure amb suspend().
synchronized (monitor) {
while (pausada && !aturada) {
monitor.wait(); // deixa anar 'monitor', queda WAITING
}
}
processarUnLot();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void processarUnLot() { /* feina */ }
}La diferència essencial amb suspend(): el fil tria on es pausa. En aquell punt l'estat del negoci és coherent i no es reté cap pany del catàleg. És el mateix principi que la interrupció cooperativa.
- Diagnòstic a la pràctica: el bolcat de fils
Arribem a la part que faràs servir de veritat. Un bolcat de fils (thread dump) és una foto instantània de tots els fils de la JVM: el seu nom, el seu estat, la seva traça de pila i els panys que tenen o esperen. És l'eina número u per diagnosticar una aplicació penjada o lenta.
12.1 Com obtenir-lo
Opció A: Ctrl+\ al terminal (Linux/macOS; a Windows, Ctrl+Break). La JVM imprimeix el bolcat a System.err. No requereix eines i no mata el procés.
Opció B: jstack, inclòs al JDK. És l'opció professional:
# 1. Localitzar el proces Java
jps -l
# 48213 com.nexussoftware.bibliotech.BiblioTechApp
# 48291 jdk.jcmd/sun.tools.jps.Jps
# 2. Bolcar els seus fils
jstack 48213
# 3. Desar-lo per analitzar-lo amb calma
jstack 48213 > /tmp/bolcat-1.txt
# 4. Prendre TRES bolcats separats per 5 segons:
# comparar els tres distingeix un fil encallat d'un que avanca
for i in 1 2 3; do jstack 48213 > /tmp/bolcat-$i.txt; sleep 5; doneOpció C: jcmd, més modern i amb més informació:
El consell operatiu que més val de tot l'apartat: pren sempre TRES bolcats separats per uns segons. Un bolcat aïllat et diu on és cada fil en aquell instant, i un fil pot estar-hi de pas. Tres bolcats on el mateix fil apareix a la mateixa línia signifiquen que està encallat, no ocupat. És la diferència entre un diagnòstic i una conjectura.
12.2 Anatomia d'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.servei.Cataleg.afegir(Cataleg.java:88)
- waiting to lock <0x000000071ab34d18> (a com.nexussoftware.bibliotech.servei.Cataleg)
at com.nexussoftware.bibliotech.persistencia.TascaImportacio.run(TascaImportacio.java:64)
at java.base/java.lang.Thread.run(Thread.java:1583)Camp a camp:
| Camp | Significat |
|---|---|
"bibliotech-importador" |
El nom del fil. Aquí es cobra el consell de 08-02 |
#21 |
Identificador del fil a la JVM |
prio=5 |
Prioritat Java (recorda 08-02: gairebé no significa res) |
cpu=1243.55ms |
CPU consumida en total. Un fil "encallat" amb CPU creixent està en un bucle, no bloquejat |
elapsed=18.32s |
Temps de vida del fil |
nid=0x5f2a |
Identificador natiu, en hexadecimal. Es creua amb top -H per veure quin fil crema CPU |
waiting for monitor entry |
Resum textual de l'estat |
java.lang.Thread.State: BLOCKED |
L'estat de l'apartat 1 |
- waiting to lock <0x...> |
Quin pany espera, amb la seva identitat i la seva classe |
at ... |
La traça de pila: on és exactament |
Les tres línies que cal mirar primer, en aquest ordre: l'estat, el - waiting to lock / - locked, i la primera línia at del teu propi codi (ignorant les de java.base).
Formes habituals:
--- Un fil TREBALLANT de veritat (consumeix CPU) ---
"bibliotech-calcul" #24 ... cpu=8420.11ms ... runnable
java.lang.Thread.State: RUNNABLE
at com.nexussoftware.bibliotech.servei.CalculadoraMultes.recalcular(CalculadoraMultes.java:52)
--- Un fil esperant E/S: RUNNABLE pero SENSE 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 fil de pool OCIOS: 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 fil ADORMIT ---
"bibliotech-monitor" #33 ... sleeping
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.base/java.lang.Thread.sleep(Native Method)Com llegir aquests quatre patrons:
RUNNABLEambcpualta i creixent entre bolcats: treballant o en un bucle infinit. Compara els tres bolcats: si la traça no canvia i la CPU puja, és un bucle sense sortida.RUNNABLEambcpubaixa ireadBytesal cim: esperant E/S. No és un problema de CPU; és el cas de tasca limitada per E/S de 08-01.WAITING (parking)agetTaskd'unThreadPoolExecutor: fil de pool esperant feina. És el normal, no el persegueixis. En veuràs a desenes a 08-05.BLOCKED (on object monitor)en diversos fils sobre el mateix<0x...>: contenció. Si a més dura entre els tres bolcats, busca qui posseeix aquell pany —apareixerà amb- locked <aquest mateix 0x...>— i per què triga tant.
- Llegir un interbloqueig detectat per la JVM
El millor dels bolcats és que la JVM detecta els interbloquejos de monitors per tu i els explica al final del bolcat. Aquest és l'aspecte que té, amb codi de BiblioTech:
public class InterbloqueigBiblioTech {
private static final Object CATALEG = new Object();
private static final Object REGISTRE = new Object();
public static void main(String[] args) {
// Fil A: cataleg -> registre
new Thread(() -> {
synchronized (CATALEG) {
dormir(200);
synchronized (REGISTRE) {
System.out.println("A: no arriba mai aqui");
}
}
}, "bibliotech-prestecs").start();
// Fil B: registre -> cataleg (ORDRE INVERS: la causa)
new Thread(() -> {
synchronized (REGISTRE) {
dormir(200);
synchronized (CATALEG) {
System.out.println("B: no arriba mai aqui");
}
}
}, "bibliotech-devolucions").start();
}
static void dormir(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}Aquest programa es penja: no imprimeix res, no llança cap excepció, no consumeix CPU i no acaba mai. És el pitjor tipus de fallada: silenciosa i total. Llança jstack sobre ell i el final del bolcat diu:
Found one Java-level deadlock:
=============================
"bibliotech-prestecs":
waiting to lock monitor 0x00007f3c14006e00 (object 0x000000071ab34d18, a java.lang.Object),
which is held by "bibliotech-devolucions"
"bibliotech-devolucions":
waiting to lock monitor 0x00007f3c14009480 (object 0x000000071ab34d28, a java.lang.Object),
which is held by "bibliotech-prestecs"
Java stack information for the threads listed above:
===================================================
"bibliotech-prestecs":
at InterbloqueigBiblioTech.lambda$main$0(InterbloqueigBiblioTech.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-devolucions":
at InterbloqueigBiblioTech.lambda$main$1(InterbloqueigBiblioTech.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.Com es llegeix, en tres passos:
Found one Java-level deadlockés la paraula clau a buscar. Si apareix, no hi ha res a discutir: hi ha un interbloqueig.- La secció de dalt dona el cicle:
prestecsespera un pany que tédevolucions, idevolucionsespera un pany que téprestecs. Un cicle de dos; poden ser de tres o més. - Les traces de pila donen les línies exactes —
InterbloqueigBiblioTech.java:14i:25— i, sobretot, la combinació- locked <A>+- waiting to lock <B>en un fil i- locked <B>+- waiting to lock <A>en l'altre. Això és la signatura de l'interbloqueig per ordre invers d'adquisició, i la solució —imposar un ordre global— és 08-04.
Dues limitacions importants de la detecció automàtica:
- Detecta interbloquejos de monitors
synchronizedi deReentrantLock(a través deThreadMXBean), però no detecta esperes mútues lògiques: dos fils esperant cadascun un senyal de l'altre ambwait()no produeixen un "deadlock" declarat, encara que l'efecte sigui el mateix. Allà només veuràs dos fils enWAITINGper sempre. - No detecta el livelock (08-04), en què els fils sí que es mouen però sense progressar.
- Inspecció des del propi programa
De vegades convé inspeccionar els fils des de dins de l'aplicació, típicament per a un endpoint de diagnòstic o per bolcar l'estat abans d'avortar.
Thread.getAllStackTraces() retorna un mapa de tots els fils vius amb les seves traces:
import java.util.Map;
public class BolcatIntern {
/** Bolca tots els fils vius i les seves traces. Util en un gestor global. */
public static void bolcarFils() {
Map<Thread, StackTraceElement[]> tots = Thread.getAllStackTraces();
System.out.println("=== BOLCAT INTERN: " + tots.size() + " fils ===");
tots.forEach((fil, traca) -> {
System.out.printf("%n\"%s\" id=%d estat=%s dimoni=%s%n",
fil.getName(), fil.threadId(), fil.getState(), fil.isDaemon());
// Nomes les 5 primeres linies: la resta rarament aporta res
int n = Math.min(5, traca.length);
for (int i = 0; i < n; i++) {
System.out.println(" at " + traca[i]);
}
if (traca.length > n) {
System.out.println(" ... " + (traca.length - n) + " mes");
}
});
}
}ThreadMXBean (de java.lang.management) dona molta més informació, inclosa la detecció programàtica d'interbloquejos:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DetectorInterbloqueigs {
private static final ThreadMXBean MX = ManagementFactory.getThreadMXBean();
/** Retorna un informe d'interbloqueig, o null si no n'hi ha cap. */
public static String detectar() {
long[] ids = MX.findDeadlockedThreads(); // null si no n'hi ha
if (ids == null) {
return null;
}
StringBuilder sb = new StringBuilder("INTERBLOQUEIG DETECTAT:\n");
for (ThreadInfo info : MX.getThreadInfo(ids, true, true)) {
sb.append(" Fil '").append(info.getThreadName())
.append("' (").append(info.getThreadState()).append(")\n")
.append(" espera : ").append(info.getLockName()).append('\n')
.append(" en mans de: ").append(info.getLockOwnerName()).append('\n');
}
return sb.toString();
}
/**
* Vigilant que comprova cada 10 segons si hi ha interbloqueig.
* Dimoni: es purament informatiu i no ha d'impedir el tancament (08-02).
*/
public static void arrencarVigilant() {
Thread vigilant = 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-vigilant-interbloqueigs");
vigilant.setDaemon(true);
vigilant.start();
}
}Un vigilant així, registrant al logger de 06-07, converteix un "l'aplicació s'ha penjat i no sabem per què" en una entrada de log que diu exactament quins dos fils i quins dos panys. Val les trenta línies que costa.
Eines gràfiques.
jconsole(inclosa al JDK) i VisualVM (descàrrega a part) mostren en temps real el nombre de fils, el seu estat, un gràfic d'activitat i un botó de "Detectar interbloqueig". Per a una primera exploració són molt més còmodes quejstack; per a una anàlisi seriosa o per a un sistema sense interfície gràfica,jstackcontinua sent l'eina. Java Flight Recorder + JDK Mission Control és l'esglaó professional, amb mostreig continu i baix impacte.
- BiblioTech: seguir l'estat dels seus fils
Aplicació pràctica: un monitor que mostra l'estat dels fils de BiblioTech mentre treballen.
package com.nexussoftware.bibliotech.infraestructura;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Monitor de fils de BiblioTech.
*
* Es un fil DIMONI: purament informatiu, no ha d'impedir el tancament
* de l'aplicacio (08-02, apartat 7).
*/
public class MonitorFils implements Runnable {
private final List<Thread> vigilats;
private final long periodeMs;
public MonitorFils(List<Thread> vigilats, long periodeMs) {
this.vigilats = List.copyOf(vigilats); // copia immutable: segur
this.periodeMs = periodeMs;
}
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
boolean algunViu = false;
StringBuilder sb = new StringBuilder("[monitor] ");
for (Thread f : vigilats) {
Thread.State e = f.getState();
sb.append(String.format("%-24s %-14s | ", f.getName(), e));
if (e != Thread.State.TERMINATED && e != Thread.State.NEW) {
algunViu = true;
}
}
System.out.println(sb);
if (!algunViu) {
System.out.println("[monitor] tots els fils han acabat");
return;
}
TimeUnit.MILLISECONDS.sleep(periodeMs);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/** Arrenca el monitor com a dimoni i retorna el seu fil. */
public static Thread arrencar(List<Thread> vigilats, long periodeMs) {
Thread t = new Thread(new MonitorFils(vigilats, periodeMs),
"bibliotech-monitor-fils");
t.setDaemon(true);
t.start();
return t;
}
}Ús, sobre la importació de 08-02 i una tasca d'avisos:
package com.nexussoftware.bibliotech.presentacio;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class DemostracioEstats {
public static void main(String[] args) throws InterruptedException {
Cataleg cataleg = new Cataleg();
Thread importador = new Thread(
new TascaImportacio(Path.of("dades/inventari.csv"), cataleg),
"bibliotech-importador");
Thread avisos = new Thread(() -> {
for (int i = 1; i <= 20; i++) {
try {
// E/S simulada: en un bolcat, aquest fil apareixeria
// en TIMED_WAITING (sleeping).
TimeUnit.MILLISECONDS.sleep(150);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "bibliotech-avisos");
// El monitor arrenca ABANS: aixi captura l'estat NEW.
MonitorFils.arrencar(List.of(importador, avisos), 400);
TimeUnit.MILLISECONDS.sleep(500);
importador.start();
avisos.start();
importador.join();
avisos.join();
TimeUnit.MILLISECONDS.sleep(500); // marge per a l'ultima linia
}
}Sortida:
[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] tots els fils han acabatEl que aquesta sortida ensenya:
- L'importador alterna entre
RUNNABLE(llegint i validant línies) iTIMED_WAITING(les pauses simulades). És el perfil d'una tasca mixta. - El fil d'avisos està gairebé sempre en
TIMED_WAITING: és el perfil d'una tasca limitada per E/S. Aquesta és exactament l'observació empírica que justifica fer servir molts més fils que nuclis per als avisos (08-01, apartat 6) i que s'explotarà a 08-05. - El monitor detecta el fi dels dos i acaba sol. Com que és dimoni, encara que no ho fes no impediria el tancament de la JVM.
Errors Comuns i Consells
Error 1: creure que RUNNABLE significa "consumint CPU". Un fil bloquejat en read() apareix com a RUNNABLE i no consumeix res. Mira la traça de pila i el camp cpu= del bolcat, no només l'estat.
Error 2: fer servir if en lloc de while al voltant de wait(). Un bug, no un estil. Despertars espuris, notifyAll amb diversos esperant, i condicions que canvien entre el senyal i el despertar: tres motius independents.
Error 3: cridar wait/notify sense el monitor. IllegalMonitorStateException en temps d'execució. El compilador no ajuda aquí.
Error 4: fer servir notify() quan hi ha fils esperant per condicions diferents. Senyal perdut i bloqueig silenciós. Fes servir notifyAll(), o Condition de 08-04 si el rendiment ho justifica.
Error 5: cridar notifyAll() al principi del bloc sincronitzat. No deixa anar el monitor: els despertats passen a BLOCKED i esperen igual. Senyalitza el més a prop possible del final del bloc.
Error 6: fer servir stop, suspend o resume. Estat corrupte i interbloquejos garantits. stop() ni tan sols funciona ja en Java 20+.
Error 7: treure un únic bolcat de fils i treure'n conclusions. Un fil pot estar de pas per aquella línia. Pren-ne tres, separats per cinc segons, i compara.
Error 8: perseguir fils WAITING en un pool. Els fils ociosos d'un ThreadPoolExecutor estan en WAITING (parking) dins de getTask(). És el seu estat normal quan no hi ha feina.
Error 9: sondejar amb sleep dins d'un bucle. Consumeix CPU per no res, i afegeix latència igual al període de sondeig. Bloqueja't en la condició: wait, BlockingQueue o CountDownLatch.
Consell 1: aprèn a llegir un bolcat abans de necessitar-lo. Provoca un interbloqueig a propòsit amb el codi de l'apartat 13 i llança-li jstack. Quan passi en producció a les tres de la matinada, agrairàs haver-ho vist abans.
Consell 2: creua nid del bolcat amb top -H -p <pid>. top -H mostra els fils amb el seu consum de CPU en decimal; converteix-lo a hexadecimal i busca'l com a nid= al bolcat. Així identifiques exactament quin fil Java està cremant un nucli.
Consell 3: instal·la un detector d'interbloquejos amb ThreadMXBean. Trenta línies i un fil dimoni converteixen una penjada misteriosa en una entrada de log concreta.
Consell 4: no facis servir wait/notify en codi nou. Fes-lo servir per entendre i per llegir codi aliè. Per escriure, fes servir BlockingQueue (08-06), CountDownLatch o Semaphore (08-05): fan el mateix, sense els tres errors clàssics d'aquest apartat.
Exercicis
Exercici 1: Observador d'estats
Escriu ObservadorEstats que llanci un fil anomenat fil-sota-observacio la feina del qual passi deliberadament per almenys quatre estats diferents: TIMED_WAITING (dormint), BLOCKED (per un pany que main reté), WAITING (per un wait() sense termini) i TERMINATED. Un segon fil, dimoni i anomenat observador, ha de mostrejar l'estat del primer cada 50 ms i imprimir només els canvis d'estat (no repetir el mateix estat en línies consecutives), juntament amb el temps transcorregut des de l'arrencada mesurat amb System.nanoTime().
Exercici 2: Cua de reserves amb wait/notifyAll
Implementa CuaReservesCoordinada, una cua acotada de reserves de BiblioTech fent servir exclusivament synchronized, wait i notifyAll (res de java.util.concurrent). Ha de tenir:
- Capacitat màxima fixa (per exemple 5).
posar(Reserva r): si està plena, espera fins que hi hagi lloc.prendre(): si està buida, espera fins que hi hagi alguna cosa.- El bucle
whilecorrecte en els dos mètodes, inotifyAlldesprés de cada modificació. - Suport d'interrupció: propagar
InterruptedException.
Escriu un main amb dos productors i tres consumidors que demostri que la cua mai no supera la seva capacitat i que ningú no obté null.
Exercici 3: Provocar i diagnosticar un interbloqueig
Escriu InterbloqueigDiagnosticat que:
- Provoqui deliberadament un interbloqueig entre dos fils anomenats
bibliotech-prestecsibibliotech-devolucions, que prenen els panysCATALEGiREGISTREen ordre oposat. - Llanci un tercer fil dimoni,
bibliotech-detector, que faci servirThreadMXBean.findDeadlockedThreads()per detectar-lo, imprimir un informe amb els noms dels fils, els panys implicats i qui posseeix cadascun, i després acabi el programa ambSystem.exit(1). - Documenti en un comentari com obtindries el mateix diagnòstic amb
jstacki quina línia buscaries.
Després, escriu una segona versió, SenseInterbloqueig, que resolgui el problema imposant un ordre global d'adquisició dels panys, i comprova que el detector ja no hi troba res.
Solucions
Solució a l'Exercici 1
import java.util.concurrent.TimeUnit;
public class ObservadorEstats {
private static final Object pany = new Object();
private static final Object senyal = new Object();
public static void main(String[] args) throws InterruptedException {
final long inici = System.nanoTime();
Thread observat = new Thread(() -> {
try {
// 1) TIMED_WAITING
TimeUnit.MILLISECONDS.sleep(400);
// 2) BLOCKED: main te 'pany' pres
synchronized (pany) {
// (a dins gairebe no fem res)
}
// 3) WAITING: espera indefinida fins que main senyalitzi
synchronized (senyal) {
senyal.wait();
}
// 4) TERMINATED en sortir de run()
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "fil-sota-observacio");
Thread observador = new Thread(() -> {
Thread.State anterior = null;
try {
while (true) {
Thread.State actual = observat.getState();
// Nomes imprimim els CANVIS: si no, la sortida
// seria illegible amb centenars de linies repetides.
if (actual != anterior) {
long ms = (System.nanoTime() - inici) / 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");
// Dimoni: es informatiu, no ha d'impedir el tancament de la JVM.
observador.setDaemon(true);
observador.start();
// Estat NEW visible durant 200 ms abans d'arrencar.
TimeUnit.MILLISECONDS.sleep(200);
observat.start();
// Als 600 ms prenem el pany 500 ms: l'observat, que
// hi arriba cap als 400 ms, quedara BLOCKED.
TimeUnit.MILLISECONDS.sleep(400);
synchronized (pany) {
TimeUnit.MILLISECONDS.sleep(500);
}
// El deixem entrar a wait() i despres el despertem.
TimeUnit.MILLISECONDS.sleep(400);
synchronized (senyal) {
senyal.notifyAll();
}
observat.join();
TimeUnit.MILLISECONDS.sleep(200); // marge per a l'ultima linia
}
}Sortida:
0 ms NEW
201 ms RUNNABLE
252 ms TIMED_WAITING
604 ms BLOCKED
1104 ms RUNNABLE
1155 ms WAITING
1508 ms TERMINATEDEls sis estats en ordre, amb els seus temps. Fixa't en el pas de BLOCKED a WAITING: passa per RUNNABLE als 1104 ms, exactament com deia el diagrama de l'apartat 2. No hi ha transicions directes entre estats d'espera.
Solució a l'Exercici 2
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.TimeUnit;
/**
* Cua acotada de reserves implementada amb synchronized/wait/notifyAll.
*
* NOTA DIDACTICA: en produccio faries servir una ArrayBlockingQueue (08-06),
* que fa exactament aixo millor i sense marge d'error. Aquesta classe existeix
* per veure el mecanisme per dins.
*/
public class CuaReservesCoordinada {
private final Deque<String> cua = new ArrayDeque<>();
private final int capacitat;
public CuaReservesCoordinada(int capacitat) {
if (capacitat <= 0) {
throw new IllegalArgumentException("la capacitat ha de ser positiva");
}
this.capacitat = capacitat;
}
/**
* Afegeix una reserva. Si la cua esta plena, espera.
* Propaga InterruptedException: qui crida decideix (08-02).
*/
public void posar(String reserva) throws InterruptedException {
synchronized (cua) {
// WHILE, no if: en despertar cal RE-COMPROVAR, perque
// un altre productor pot haver omplert el forat abans que nosaltres.
while (cua.size() == capacitat) {
cua.wait();
}
cua.addLast(reserva);
// notifyAll i no notify: en aquesta cua hi esperen fils per DUES
// condicions diferents (plena i buida). Amb notify podriem
// despertar un productor quan el que pot avancar es
// un consumidor, i perdre el senyal (apartat 9).
cua.notifyAll();
}
}
/** Extreu una reserva. Si la cua esta buida, espera. Mai retorna null. */
public String prendre() throws InterruptedException {
synchronized (cua) {
while (cua.isEmpty()) {
cua.wait();
}
String r = cua.pollFirst();
cua.notifyAll();
return r;
}
}
public int mida() {
synchronized (cua) {
return cua.size();
}
}
// --- Demostracio ---
public static void main(String[] args) throws InterruptedException {
final int CAPACITAT = 5;
final int PER_PRODUCTOR = 10;
CuaReservesCoordinada cua = new CuaReservesCoordinada(CAPACITAT);
Runnable productor = () -> {
String jo = Thread.currentThread().getName();
try {
for (int i = 1; i <= PER_PRODUCTOR; i++) {
cua.posar(jo + "-reserva-" + i);
System.out.printf(" [+] %-14s posa %2d (mida=%d)%n",
jo, i, cua.mida());
TimeUnit.MILLISECONDS.sleep(30);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
Runnable consumidor = () -> {
String jo = Thread.currentThread().getName();
try {
while (!Thread.currentThread().isInterrupted()) {
String r = cua.prendre();
// Sense 'while' a prendre(), aquesta comprovacio fallaria.
if (r == null) {
throw new IllegalStateException("null: bug de coordinacio");
}
System.out.printf(" [-] %-14s pren %-28s (mida=%d)%n",
jo, r, cua.mida());
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();
// Deixem que els consumidors buidin i els aturem amb interrupt.
TimeUnit.MILLISECONDS.sleep(1000);
c1.interrupt(); c2.interrupt(); c3.interrupt();
c1.join(); c2.join(); c3.join();
System.out.println("Reserves pendents al final: " + cua.mida());
}
}Sortida (fragment):
[+] productor-1 posa 1 (mida=1)
[+] productor-2 posa 1 (mida=1)
[-] consumidor-1 pren productor-1-reserva-1 (mida=1)
[-] consumidor-2 pren productor-2-reserva-1 (mida=0)
[+] productor-1 posa 2 (mida=1)
...
Reserves pendents al final: 0Tres punts didàctics:
- La mida mai no supera 5. La condició
while (cua.size() == capacitat)aposarho garanteix. Si els productors fossin molt més ràpids, es bloquejarien en lloc de fer créixer la cua sense límit: això és contrapressió, i és una propietat valuosíssima que es reprèn ambBlockingQueuea 08-06. - Cap consumidor no obté
null. Elwhileaprendreho garanteix. Canvia aquellwhileper unifi executa: amb tres consumidors inotifyAll, laIllegalStateExceptionsalta en segons. - La mida impresa pot no quadrar amb l'operació.
System.out.printfs'executa fora del bloc sincronitzat, així que entre posar i consultar la mida un altre fil ha pogut actuar. No és un bug de la cua: és la demostració que una lectura fora de la secció crítica és només una foto d'un instant ja passat. És exactament el problema de "comprovar-després-actuar" de 08-06.
Solució a l'Exercici 3
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.TimeUnit;
/**
* Provoca un interbloqueig i el diagnostica des de dins del programa.
*
* DIAGNOSTIC EXTERN EQUIVALENT:
* 1) jps -l -> localitzar el pid
* 2) jstack <pid> > bolcat.txt -> bolcar els fils
* 3) buscar a bolcat.txt la linia "Found one Java-level deadlock:"
* A sota apareixen els dos fils, el pany que espera cadascun
* i qui el posseeix, mes les traces amb "- locked" i
* "- waiting to lock" del mateix parell d'adreces en ordre invers.
*/
public class InterbloqueigDiagnosticat {
// Dos panys que representen dos recursos del domini.
private static final Object CATALEG = new Object();
private static final Object REGISTRE = new Object();
public static void main(String[] args) {
arrencarDetector();
// Fil A: CATALEG -> REGISTRE
new Thread(() -> {
synchronized (CATALEG) {
dormir(300); // deixa temps a l'altre fil
synchronized (REGISTRE) { // es queda BLOCKED aqui
System.out.println("A completat (no passa mai)");
}
}
}, "bibliotech-prestecs").start();
// Fil B: REGISTRE -> CATALEG <-- ORDRE INVERS: la causa arrel
new Thread(() -> {
synchronized (REGISTRE) {
dormir(300);
synchronized (CATALEG) { // es queda BLOCKED aqui
System.out.println("B completat (no passa mai)");
}
}
}, "bibliotech-devolucions").start();
}
/** Vigilant dimoni que detecta interbloquejos i avorta amb informe. */
private static void arrencarDetector() {
Thread detector = new Thread(() -> {
ThreadMXBean mx = ManagementFactory.getThreadMXBean();
while (!Thread.currentThread().isInterrupted()) {
long[] ids = mx.findDeadlockedThreads(); // null si no n'hi ha
if (ids != null) {
System.err.println();
System.err.println("========================================");
System.err.println(" INTERBLOQUEIG DETECTAT (" + ids.length + " fils)");
System.err.println("========================================");
// true, true: incloure monitors i sincronitzadors posseits
for (ThreadInfo info : mx.getThreadInfo(ids, true, true)) {
System.err.printf("%nFil : %s (%s)%n",
info.getThreadName(), info.getThreadState());
System.err.printf("Espera : %s%n", info.getLockName());
System.err.printf("Posseit per: %s%n", info.getLockOwnerName());
System.err.println("Traca (2 primeres linies):");
StackTraceElement[] traca = info.getStackTrace();
for (int i = 0; i < Math.min(2, traca.length); i++) {
System.err.println(" at " + traca[i]);
}
}
System.err.println();
System.err.println("CAUSA: els dos fils adquireixen els mateixos dos");
System.err.println("panys en ORDRE OPOSAT. Solucio a 08-04:");
System.err.println("imposar un ordre global d'adquisicio.");
System.exit(1);
}
dormir(500);
}
}, "bibliotech-detector");
detector.setDaemon(true); // no ha d'impedir el tancament
detector.start();
}
private static void dormir(long ms) {
try {
TimeUnit.MILLISECONDS.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Sortida:
========================================
INTERBLOQUEIG DETECTAT (2 fils)
========================================
Fil : bibliotech-prestecs (BLOCKED)
Espera : java.lang.Object@5b6f7412
Posseit per: bibliotech-devolucions
Traca (2 primeres linies):
at InterbloqueigDiagnosticat.lambda$main$0(InterbloqueigDiagnosticat.java:31)
at java.base/java.lang.Thread.run(Thread.java:1583)
Fil : bibliotech-devolucions (BLOCKED)
Espera : java.lang.Object@2d1ef81a
Posseit per: bibliotech-prestecs
Traca (2 primeres linies):
at InterbloqueigDiagnosticat.lambda$main$1(InterbloqueigDiagnosticat.java:42)
at java.base/java.lang.Thread.run(Thread.java:1583)
CAUSA: els dos fils adquireixen els mateixos dos panys en ORDRE OPOSAT.
Solucio a 08-04: imposar un ordre global d'adquisicio.I la versió corregida, amb ordre global d'adquisició:
/**
* Mateixa funcionalitat, sense interbloqueig possible.
*
* REGLA: TOTS els fils adquireixen els panys en el MATEIX ordre,
* sempre CATALEG abans que REGISTRE. Amb un ordre total sobre els
* panys no es pot formar un cicle d'espera, i sense cicle no hi ha
* interbloqueig. Es la solucio que es desenvolupa a 08-04.
*/
public class SenseInterbloqueig {
private static final Object CATALEG = new Object();
private static final Object REGISTRE = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable prestar = () -> {
synchronized (CATALEG) { // 1r CATALEG
dormir(300);
synchronized (REGISTRE) { // 2n REGISTRE
System.out.println("[" + Thread.currentThread().getName()
+ "] prestec registrat");
}
}
};
Runnable retornar = () -> {
synchronized (CATALEG) { // 1r CATALEG (abans era REGISTRE)
dormir(300);
synchronized (REGISTRE) { // 2n REGISTRE
System.out.println("[" + Thread.currentThread().getName()
+ "] devolucio registrada");
}
}
};
Thread a = new Thread(prestar, "bibliotech-prestecs");
Thread b = new Thread(retornar, "bibliotech-devolucions");
a.start(); b.start();
a.join(); b.join();
System.out.println("Els dos fils han acabat: NO hi ha interbloqueig");
}
private static void dormir(long ms) {
try { TimeUnit.MILLISECONDS.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}Sortida:
[bibliotech-prestecs] prestec registrat
[bibliotech-devolucions] devolucio registrada
Els dos fils han acabat: NO hi ha interbloqueigEl canvi és d'una sola línia —invertir l'ordre dels dos synchronized a retornar— i elimina completament una classe de fallada que pot tombar una aplicació en producció sense deixar rastre. És la millor relació cost/benefici de tot el mòdul, i a 08-04 es converteix en una regla de disseny explícita.
Conclusió
Ja no veus un fil com una caixa negra que arrenca i acaba: saps exactament on pot ser i què el mou d'un lloc a un altre.
Els sis estats de Thread.State i les seves transicions: NEW abans de start(), RUNNABLE quan està executant o llest per executar, BLOCKED esperant un monitor, WAITING esperant un senyal, TIMED_WAITING esperant amb termini, i TERMINATED com a estat terminal i irreversible —que és la raó profunda que un Thread no es pugui reutilitzar i que existeixin els pools—. Amb l'observació estructural que ordena el diagrama: totes les transicions passen per RUNNABLE; un fil que es desperta de wait() ha de readquirir el monitor, i en aquell moment pot quedar BLOCKED.
Coneixes els dos matisos que separen qui sap llegir un sistema en producció de qui no. El primer: RUNNABLE no significa "consumint CPU", perquè agrupa executar, esperar torn i —això sorprèn sempre— estar bloquejat en E/S clàssica; cal mirar la traça de pila i el camp cpu=, no l'estat. El segon: BLOCKED és "no em deixen entrar" i WAITING és "espero que m'avisin", contenció davant de coordinació, i aquesta distinció converteix un bolcat illegible en un diagnòstic.
Domines el mecanisme de coordinació de baix nivell: wait, notify i notifyAll viuen a Object, es criden sempre amb el monitor a la mà —o IllegalMonitorStateException—, i wait() deixa anar el monitor mentre que sleep() no, que és la diferència que defineix per a què serveix cadascun. I tens la regla que més s'incompleix: wait() va sempre dins d'un while, mai dins d'un if, per tres motius independents —despertars espuris, notifyAll amb diversos esperant i un sol element, i condicions que canvien entre el senyal i el despertar—, amb la idea que els resumeix: notify no diu "la teva condició es compleix", diu "torna a mirar". I la recomanació derivada: notifyAll per defecte, perquè notify amb fils esperant per condicions diferents produeix senyals perduts i bloquejos silenciosos.
Saps per què stop, suspend i resume estan retirats des de 1998: el primer deixa anar tots els monitors enmig d'una operació i deixa estructures corruptes sense donar cap error; els altres dos retenen els monitors mentre el fil està pausat, cosa que produeix interbloquejos perfectes. L'alternativa és la mateixa idea en els dos casos: el fil tria on s'atura, que és la cancel·lació cooperativa de 08-02 i el punt de pausa explícit amb wait/notify.
I, sobretot, saps diagnosticar. Obtenir un bolcat amb Ctrl+\, jstack o jcmd; llegir una entrada camp a camp —nom, estat, cpu=, - locked i - waiting to lock, traça—; reconèixer els quatre perfils habituals —treballant, esperant E/S, fil de pool ociós, bloquejat—; i trobar el Found one Java-level deadlock que la JVM escriu per tu, amb el cicle d'espera i les línies exactes del codi. Amb la regla operativa que evita conclusions falses: tres bolcats separats per uns segons, perquè un fil pot estar de pas però no pot estar de pas tres vegades a la mateixa línia. I amb Thread.getAllStackTraces i ThreadMXBean.findDeadlockedThreads() per diagnosticar des de dins, més jconsole, VisualVM i JFR per fer-ho amb interfície gràfica.
BiblioTech ja es pot observar. MonitorFils mostra l'estat dels fils d'importació i d'avisos mentre treballen, i aquella sortida ensenya alguna cosa que fins ara era teoria: l'importador alterna RUNNABLE i TIMED_WAITING —tasca mixta—, mentre que el fil d'avisos està gairebé sempre en TIMED_WAITING —tasca limitada per E/S—. És la confirmació empírica de la taula de 08-01 i la justificació de tot el que ve.
Perquè ara ja saps observar el problema, i arriba el moment de resoldre'l. Has vist un interbloqueig real, has vist tres consumidors obtenir null per un if mal posat, i continues tenint pendent el comptador que perdia mig milió d'increments des de 08-01.
A la propera lliçó, Sincronització —la lliçó central del mòdul—, es resol tot això. Veuràs per què comptador++ són en realitat tres operacions i quin entrellaçat exacte perd els increments; el model de memòria de Java, amb les memòries cau per nucli, la reordenació d'instruccions i la relació happens-before que ho governa tot; què garanteix volatile —visibilitat— i què no —atomicitat—, amb els dos exemples que ho demostren; synchronized en totes les seves formes, el monitor intrínsec, el bloqueig privat i la granularitat correcta; l'interbloqueig reproduïble i les seves dues solucions —ordre global i tryLock amb termini—; ReentrantLock amb les seves Condition, ReadWriteLock per a estructures que es llegeixen molt i s'escriuen poc, i la millor estratègia de totes: no compartir. En acabar, el Cataleg i el RegistrePrestecs de BiblioTech seran segurs per a diversos fils, i el cas C de 08-01 —dos empleats treballant alhora— deixarà de ser una amenaça.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
