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ó: BlockingQueue i CountDownLatch ho 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

  1. Els sis estats de Thread.State
  2. El diagrama complet del cicle de vida
  3. NEW i TERMINATED: els extrems
  4. RUNNABLE: el matís que confon tothom
  5. BLOCKED davant de WAITING: la distinció que fa llegible un bolcat
  6. TIMED_WAITING: esperar amb termini
  7. wait, notify i notifyAll
  8. El bucle while obligatori
  9. notify davant de notifyAll
  10. yield i onSpinWait
  11. Els mètodes retirats: stop, suspend, resume
  12. Diagnòstic a la pràctica: el bolcat de fils
  13. Llegir un interbloqueig detectat per la JVM
  14. Inspecció des del propi programa
  15. BiblioTech: seguir l'estat dels seus fils
  16. Errors Comuns i Consells
  17. Exercicis

  1. Els sis estats de Thread.State

Thread.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ça IllegalThreadStateException. 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.

  1. 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()     : TERMINATED

Aquest programa és intrínsecament fràgil i és honest dir-ho: depèn que els sleep de 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.

  1. NEW i TERMINATED: els extrems

NEW é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().

  1. RUNNABLE: el matís que confon tothom

Molts manuals dibuixen un estat RUNNING separat de RUNNABLE. Java no el té. Thread.State.RUNNABLE agrupa dues situacions molt diferents:

  1. Executant ara mateix en un nucli.
  2. 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.socketRead0 o FileInputStream.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—.

  1. BLOCKED davant de WAITING: la distinció que fa llegible un bolcat

Aquesta é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:

esperador  : WAITING
acaparador : TIMED_WAITING
bloquejat  : BLOCKED

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
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

  1. TIMED_WAITING: esperar amb termini

TIMED_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

  1. wait, notify i notifyAll

Aquests 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 synchronized sobre aquell mateix objecte. Si no, es llança IllegalMonitorStateException en 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:

  1. El consumidor comprova cua.isEmpty()true.
  2. Justament aquí el productor afegeix un element i crida notify().
  3. 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-0000000001

Un 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. synchronized s'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 cridar wait, notify o notifyAll sobre un objecte, cal estar dins d'un synchronized sobre aquell mateix objecte.

  1. El bucle while obligatori

Aquesta é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 bucle while que comprova la condició. Mai dins d'un if.

// 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 buida

Dos 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.

  1. notify davant de notifyAll

notify() 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 Condition de ReentrantLock, que permeten signal() sobre exactament el grup correcte de fils. Es veuen a 08-04.

  1. yield i onSpinWait

Dos 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ó.

  1. Els mètodes retirats: stop, suspend, resume

Thread 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.

  1. 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; done

Opció C: jcmd, més modern i amb més informació:

jcmd 48213 Thread.print
jcmd 48213 Thread.print -l    # inclou els panys posseits

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:

  • RUNNABLE amb cpu alta 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.
  • RUNNABLE amb cpu baixa i readBytes al cim: esperant E/S. No és un problema de CPU; és el cas de tasca limitada per E/S de 08-01.
  • WAITING (parking) a getTask d'un ThreadPoolExecutor: 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.

  1. 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:

  1. Found one Java-level deadlock és la paraula clau a buscar. Si apareix, no hi ha res a discutir: hi ha un interbloqueig.
  2. La secció de dalt dona el cicle: prestecs espera un pany que té devolucions, i devolucions espera un pany que té prestecs. Un cicle de dos; poden ser de tres o més.
  3. Les traces de pila donen les línies exactesInterbloqueigBiblioTech.java:14 i :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 synchronized i de ReentrantLock (a través de ThreadMXBean), però no detecta esperes mútues lògiques: dos fils esperant cadascun un senyal de l'altre amb wait() no produeixen un "deadlock" declarat, encara que l'efecte sigui el mateix. Allà només veuràs dos fils en WAITING per sempre.
  • No detecta el livelock (08-04), en què els fils sí que es mouen però sense progressar.

  1. 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 que jstack; per a una anàlisi seriosa o per a un sistema sense interfície gràfica, jstack continua sent l'eina. Java Flight Recorder + JDK Mission Control és l'esglaó professional, amb mostreig continu i baix impacte.

  1. 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 acabat

El que aquesta sortida ensenya:

  • L'importador alterna entre RUNNABLE (llegint i validant línies) i TIMED_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 while correcte en els dos mètodes, i notifyAll despré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:

  1. Provoqui deliberadament un interbloqueig entre dos fils anomenats bibliotech-prestecs i bibliotech-devolucions, que prenen els panys CATALEG i REGISTRE en ordre oposat.
  2. Llanci un tercer fil dimoni, bibliotech-detector, que faci servir ThreadMXBean.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 amb System.exit(1).
  3. Documenti en un comentari com obtindries el mateix diagnòstic amb jstack i 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  TERMINATED

Els 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: 0

Tres punts didàctics:

  1. La mida mai no supera 5. La condició while (cua.size() == capacitat) a posar ho 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 amb BlockingQueue a 08-06.
  2. Cap consumidor no obté null. El while a prendre ho garanteix. Canvia aquell while per un if i executa: amb tres consumidors i notifyAll, la IllegalStateException salta en segons.
  3. La mida impresa pot no quadrar amb l'operació. System.out.printf s'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 interbloqueig

El 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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats