A la lliçó anterior vas acabar amb un comptador que perdia mig milió d'increments. Abans d'arreglar-lo cal aprendre a manejar bé l'eina que el va trencar: el fil.
Aquesta lliçó és la dels mecanismes. Veuràs les quatre formes de crear un fil en Java i per què només una d'elles és recomanable; l'error clàssic de cridar run() quan volies start(), demostrat amb una sortida que no deixa lloc a dubtes; com esperar que un fil acabi amb join(); per què posar nom als fils no és cosmètica sinó una necessitat operativa; i —el més important de tota la lliçó— el protocol d'interrupció, que és l'únic mecanisme de cancel·lació cooperativa que té Java i el que permetrà que la importació de catàleg de BiblioTech deixi de ser un bloc de vuit segons que no es pot aturar.
En acabar, ImportadorCataleg correrà en un fil propi anomenat bibliotech-importador, informarà del seu progrés i respondrà a una petició de cancel·lació en menys de mig segon, deixant el catàleg en un estat coherent.
Avís sobre el que aprens aquí. Gairebé res d'aquesta lliçó no ho escriuràs així en producció: a 08-05 apareixen els executors, que creen i reutilitzen els fils per tu. Però un executor és una capa sobre això, i quan falli —i fallarà— hauràs de raonar en termes de fils,
joini interrupció. Aquesta és la capa de sota.
Contingut
- Les quatre formes de crear un fil
- Per què
Runnableguanya a estendreThread start()davant derun(): l'error clàssic- El cicle bàsic: crear, arrencar, esperar amb
join() - Posar nom als fils: per què és imprescindible
- Prioritats i per què gairebé mai no serveixen
- Fils dimoni: el moment de cridar
setDaemon - Dormir:
Thread.sleepiTimeUnit - Interrupció: el protocol de cancel·lació
- Excepcions en un fil: no van on et penses
- Passar dades i recollir resultats
- BiblioTech: la importació al seu propi fil, cancel·lable
- El que gairebé mai no faràs en producció
- Errors Comuns i Consells
- Exercicis
- Les quatre formes de crear un fil
Java ofereix quatre maneres d'associar un tros de codi a un fil. Totes acaben en el mateix —un objecte Thread amb un run() per executar— però difereixen molt en qualitat de disseny.
Forma 1: estendre Thread i sobreescriure run().
public class FilAvisos extends Thread {
private final int totalAvisos;
public FilAvisos(int totalAvisos) {
super("bibliotech-avisos"); // el nom del fil
this.totalAvisos = totalAvisos;
}
@Override
public void run() {
for (int i = 1; i <= totalAvisos; i++) {
System.out.println(getName() + " enviant avis " + i);
}
}
}
// Us:
FilAvisos f = new FilAvisos(3);
f.start();Aquí la classe és un fil. Funciona, és el primer que s'ensenya a tot arreu, i és el que menys hauries de fer servir.
Forma 2: implementar Runnable i passar-lo al constructor de Thread.
public class TascaAvisos implements Runnable {
private final int totalAvisos;
public TascaAvisos(int totalAvisos) {
this.totalAvisos = totalAvisos;
}
@Override
public void run() {
for (int i = 1; i <= totalAvisos; i++) {
System.out.println(Thread.currentThread().getName()
+ " enviant avis " + i);
}
}
}
// Us: la TASCA es TascaAvisos; el MECANISME es Thread.
Thread f = new Thread(new TascaAvisos(3), "bibliotech-avisos");
f.start();Aquí la classe descriu una feina; qui l'executa és una altra decisió. Aquesta és la forma recomanada, i l'apartat 2 explica per què.
Forma 3: una lambda que implementa Runnable.
Runnable és una interfície funcional —un únic mètode abstracte void run()—, exactament el tipus d'interfície que vas veure a 04-05 i 04-06. Així que es pot escriure com a lambda:
Thread f = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName()
+ " enviant avis " + i);
}
}, "bibliotech-avisos");
f.start();És la forma més concisa i l'habitual per a tasques curtes. Recorda de 04-05 que la lambda captura les variables de l'entorn, i que només pot capturar variables efectivament finals: si intentes modificar dins de la lambda un int declarat fora, el compilador t'ho impedeix. Això no és un caprici —és precisament el que evita una condició de cursa sobre la pila d'un fil que potser ja no existeix.
Forma 4: una classe anònima.
Thread f = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Executant a " + Thread.currentThread().getName());
}
}, "bibliotech-avisos");És la forma 3 escrita a l'antiga (04-04). Avui només té sentit quan la implementació necessita estat propi o diversos mètodes, cosa que amb Runnable no passa. Amb una interfície d'un sol mètode, la lambda sempre guanya.
Comparació:
| Forma | Gasta l'herència | Reutilitzable amb executors | Verbositat | Quan fer-la servir |
|---|---|---|---|---|
Estendre Thread |
Sí | No (és un fil, no una tasca) | Mitjana | Gairebé mai; només si necessites canviar el comportament del propi fil |
Implementar Runnable |
No | Sí | Mitjana | Tasques amb estat, nom i proves pròpies |
Lambda Runnable |
No | Sí | Mínima | Tasques curtes, el cas més freqüent |
| Classe anònima | No | Sí | Alta | Llegat, o si necessites camps propis sense crear una classe |
- Per què
Runnable guanya a estendre Thread
Runnable guanya a estendre ThreadHi ha tres raons, i la tercera és la de pes.
Raó 1: l'herència és un recurs únic i escàs. Java no té herència múltiple de classes. Si FilAvisos extends Thread, ja no pot estendre res més. I a BiblioTech això fa mal: si demà vols que la teva tasca estengui una classe base TascaBiblioTech amb la política d'errors de 06-07, no pots.
Raó 2: separa la tasca del mecanisme. És exactament la discussió d'herència davant de composició de 03-05. extends Thread diu "aquesta classe és un fil", cosa que és falsa: TascaAvisos no és un fil, és una feina. Que aquesta feina l'executi un fil, dos fils, un pool o el fil actual hauria de ser una decisió independent de la definició de la feina.
Raó 3, la decisiva: Runnable és la moneda de canvi de tota l'API de concurrència.
Runnable tasca = () -> Cataleg.recalcularEstadistiques();
// 1. Executar-la en un fil nou.
new Thread(tasca, "estadistiques").start();
// 2. Executar-la en un pool (08-05).
executor.submit(tasca);
// 3. Programar-la cada 10 minuts (08-05).
programador.scheduleAtFixedRate(tasca, 0, 10, TimeUnit.MINUTES);
// 4. Executar-la en apagar la JVM (shutdown hook del modul 7).
Runtime.getRuntime().addShutdownHook(new Thread(tasca, "tancament"));
// 5. Executar-la aqui mateix, sense fils, per a una prova unitaria.
tasca.run();Cinc destins completament diferents per a la mateixa tasca, sense tocar una línia de la tasca. Amb extends Thread no en pots fer cap dels quatre primers, i el cinquè —provar la tasca sense crear fils— és el que més agrairàs quan arribis a JUnit a 11-04: provar lògica de negoci arrencant fils és lent i intermitent; provar el run() d'un Runnable cridant-lo directament és una prova normal i determinista.
Regla. Escriu tasques, no fils. Deixa que qui les fa servir decideixi on s'executen.
start() davant de run(): l'error clàssic
start() davant de run(): l'error clàssicAquest és l'error de principiant més freqüent i més silenciós del tema, perquè compila, s'executa i no dona cap error. Simplement no hi ha concurrència.
run()és un mètode normal. Cridar-lo executa el codi al fil que fa la crida, com qualsevol mètode.start()demana a la JVM que creï un fil nou del sistema operatiu i que aquell fil executirun(). Retorna immediatament.
public class StartDavantDeRun {
static Runnable tasca = () ->
System.out.println(" tasca executant-se a: "
+ Thread.currentThread().getName());
public static void main(String[] args) throws InterruptedException {
System.out.println("main executant-se a: "
+ Thread.currentThread().getName());
System.out.println("\n--- Cridant run() (INCORRECTE) ---");
Thread f1 = new Thread(tasca, "fil-A");
f1.run(); // NO crea cap fil
System.out.println(" estat de fil-A: " + f1.getState());
System.out.println("\n--- Cridant start() (CORRECTE) ---");
Thread f2 = new Thread(tasca, "fil-B");
f2.start(); // crea un fil de veritat
f2.join();
System.out.println(" estat de fil-B: " + f2.getState());
}
}Sortida:
main executant-se a: main
--- Cridant run() (INCORRECTE) ---
tasca executant-se a: main
estat de fil-A: NEW
--- Cridant start() (CORRECTE) ---
tasca executant-se a: fil-B
estat de fil-B: TERMINATEDLes dues línies que delaten l'error:
- Amb
run(), la tasca imprimeixmain: es va executar al fil principal. No hi va haver paral·lelisme, no hi va haver receptivitat, no hi va haver res. - L'estat de
fil-Acontinua sentNEW: aquell objecteThreadno va arrencar mai. Es va quedar allà, construït i sense fer servir. Els estats es detallen a 08-03.
Dues regles relacionades:
start()sobre un fil ja arrencat llançaIllegalThreadStateException. UnThreadés d'un sol ús: s'arrenca una vegada i, en acabar, ja no es pot reutilitzar. Si necessites repetir la tasca, crees un altreThread(o, millor, fas servir un pool).start()retorna immediatament, no quan la tasca acaba. El codi després destart()continua corrent en paral·lel amb la tasca. Esperar és feina dejoin().
Com detectar-ho en una revisió de codi. Busca
.run()al codi de producció. Gairebé qualsevol.run()explícit sobre unThreadés un error; sobre unRunnablede vegades és intencional (execució en línia), però mereix un comentari que ho justifiqui.
- El cicle bàsic: crear, arrencar, esperar amb
join()
join()El cicle mínim té tres passos i un parany.
import java.util.concurrent.TimeUnit;
public class CicleBasic {
public static void main(String[] args) throws InterruptedException {
// 1. CREAR: nomes construeix un objecte. Encara no s'executa res.
Thread importador = new Thread(() -> {
System.out.println("[importador] comencant");
dormir(1500);
System.out.println("[importador] acabat");
}, "bibliotech-importador");
// 2. ARRENCAR: a partir d'aqui hi ha dos fluxos d'execucio.
importador.start();
System.out.println("[main] el menu continua viu mentre s'importa");
// 3. ESPERAR: main es queda aturat fins que el fil acaba.
importador.join();
System.out.println("[main] importacio confirmada, ja puc fer servir el resultat");
}
static void dormir(long ms) {
try {
TimeUnit.MILLISECONDS.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Sortida:
[importador] comencant
[main] el menu continua viu mentre s'importa
[importador] acabat
[main] importacio confirmada, ja puc fer servir el resultatEl parany: sense join(), el resultat pot no estar llest. És un error molt comú:
Thread f = new Thread(() -> informe = calcularInforme());
f.start();
System.out.println(informe); // BUG: probablement null, el fil encara no ha acabatjoin() no és només "esperar": també és una barrera de memòria. Tot el que el fil va escriure abans d'acabar és visible per a qui fa join() després. Aquesta és la primera relació happens-before concreta que veus, i s'explica formalment a 08-04. Sense join() (o algun altre mecanisme de sincronització), no només pots llegir abans d'hora: pots llegir un valor obsolet encara que el fil ja hagués acabat.
join() amb temps límit. La sobrecàrrega join(long millis) espera com a molt aquell temps:
Thread importador = new Thread(new TascaImportador(), "bibliotech-importador");
importador.start();
importador.join(5000); // espera com a maxim 5 segons
if (importador.isAlive()) {
// El join va acabar per TEMPS, no perque el fil acabes.
// COMPTE: join(long) NO llanca excepcio en exhaurir-se el termini,
// ni cancella res. Cal comprovar-ho amb isAlive().
System.out.println("La importacio triga massa; sollicitant cancellacio");
importador.interrupt(); // apartat 9
importador.join(1000); // marge perque acabi netament
if (importador.isAlive()) {
System.out.println("El fil no respon a la interrupcio");
}
} else {
System.out.println("Importacio completada dins del termini");
}Detall important que sorprèn tothom la primera vegada: join(5000) no et diu si el fil va acabar o si es va exhaurir el termini. Retorna void. L'única forma de distingir-ho és isAlive() just després. És una API antiga i maldestra; a 08-05 veuràs Future.get(timeout), que sí que llança TimeoutException i és el que faràs servir a la pràctica.
| Mètode | Què fa | Quan fer-lo servir |
|---|---|---|
join() |
Espera indefinidament que el fil acabi | Quan la terminació està garantida |
join(ms) |
Espera com a màxim ms mil·lisegons |
Quan vols un termini; comprovar isAlive() després |
isAlive() |
Ha arrencat i encara no ha acabat? | Diagnòstic i comprovació després de join(ms) |
interrupt() |
Sol·licita la cancel·lació cooperativa | Apartat 9 |
join()llançaInterruptedException. Qui espera pot al seu torn ser interromput. Tracta-la amb el protocol de l'apartat 9, mai amb uncatchbuit.
- Posar nom als fils: per què és imprescindible
Un fil sense nom rep Thread-0, Thread-1, Thread-2… Això és suficient per a un exemple de tres línies i absolutament inútil en un sistema real.
// MALAMENT: noms automatics
new Thread(tascaImportacio).start();
new Thread(tascaAvisos).start();
new Thread(tascaCopies).start();
// BE: el nom diu què està fent
new Thread(tascaImportacio, "bibliotech-importador").start();
new Thread(tascaAvisos, "bibliotech-avisos").start();
new Thread(tascaCopies, "bibliotech-copies").start();Amb noms, un bolcat de fils (08-03) o una traça d'error diuen immediatament quina part del sistema està implicada:
"bibliotech-importador" #21 prio=5 os_prio=0 tid=0x... nid=0x... waiting on condition
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.base/java.lang.Thread.sleep(Native Method)
at com.nexussoftware.bibliotech.persistencia.ImportadorCataleg.llegir(...)Sense nom, aquesta mateixa línia comença per "Thread-3" i t'obliga a deduir de la traça què és. Amb vint fils, no és viable.
Convenció recomanada per al projecte: <aplicacio>-<funcio>[-<index>].
bibliotech-importadorbibliotech-avisos-1,bibliotech-avisos-2, …bibliotech-programador
Tres motius concrets pels quals això és una necessitat i no un adorn:
- Bolcats de fils: és l'única forma de llegir un bolcat d'una aplicació amb molts fils.
- Logs:
RegistreOperacions.tracaFilde l'exercici de 08-01 inclou el nom del fil; sense noms significatius, la traça no serveix. - Monitoratge:
jconsolei VisualVM llisten fils per nom; un panell ple depool-1-thread-7no diu de quin pool és.
A 08-05 veuràs ThreadFactory, que és la forma d'aconseguir que els fils d'un pool també tinguin noms decents, perquè per defecte s'anomenen pool-1-thread-1 i això torna a ser inútil quan hi ha tres pools.
// El nom es pot fixar al constructor o despres,
// pero abans de start() (despres funciona, encara que confon els logs previs).
Thread f = new Thread(tasca);
f.setName("bibliotech-importador");
f.start();
// I es consulta des de dins:
System.out.println(Thread.currentThread().getName());
- Prioritats i per què gairebé mai no serveixen
Thread té un camp de prioritat entre 1 i 10:
Thread.MIN_PRIORITY // 1
Thread.NORM_PRIORITY // 5 (per defecte)
Thread.MAX_PRIORITY // 10
Thread f = new Thread(tasca, "bibliotech-avisos");
f.setPriority(Thread.MAX_PRIORITY);
f.start();Sembla que serveixi per dir "aquest fil és més important". A la pràctica, no hi confiïs, per quatre motius:
- És un suggeriment, no una ordre. La JVM tradueix la prioritat a la del sistema operatiu, que és qui decideix. La pot ignorar completament.
- El mapatge depèn del sistema. Windows té 7 nivells útils; Linux, amb el planificador per defecte, pràcticament ignora les prioritats dels fils d'usuari sense permisos especials. El mateix programa es comporta diferent a cada sistema.
- No garanteix ordre d'execució. Un fil de prioritat 1 es pot executar abans que un de prioritat 10. Si la teva correcció depèn d'això, el teu programa és incorrecte.
- Convida a la inanició. Si un fil de prioritat alta no cedeix mai, un de prioritat baixa pot no executar-se mai —el tercer perill de 08-01—.
// ANTIPATRO: "resoldre" una condicio de cursa amb prioritats.
escriptor.setPriority(Thread.MAX_PRIORITY);
lector.setPriority(Thread.MIN_PRIORITY);
// Aixo NO sincronitza res. Nomes fa la fallada mes rara i
// per tant mes dificil de diagnosticar. La solucio es 08-04.Què fer al seu lloc: si necessites que certa feina no competeixi amb la feina important, no li baixis la prioritat: posa-la en un pool separat amb pocs fils (08-05). Això sí que és un control real i portable.
Ús legítim i gairebé únic: baixar la prioritat de fils de manteniment no urgents (neteja de memòria cau, recollida de mètriques) com a pista que no importa si van lents. Mai com a mecanisme de correcció.
- Fils dimoni: el moment de cridar
setDaemon
setDaemonJa vas veure a 08-01 la regla —la JVM acaba quan no queda cap fil no dimoni—. Aquí va el detall d'ús.
Thread monitor = new Thread(() -> {
while (true) {
System.out.println("[monitor] fils vius: " + Thread.activeCount());
dormir(1000);
}
}, "bibliotech-monitor");
monitor.setDaemon(true); // ABANS de start(). Despres: IllegalThreadStateException
monitor.start();Regles pràctiques:
- Sempre abans de
start(). Després llançaIllegalThreadStateException. - S'hereta. Un fil creat des d'un fil dimoni neix dimoni. És una font de sorpreses: si arrenques un fil de treball des de dins d'un dimoni, aquell fil tampoc no impedirà l'apagada.
- Un dimoni no executa el seu
finallyen apagar-se la JVM. No hi ha cap garantia que s'executi res. Per tant: res que escrigui fitxers, tanqui recursos o confirmi transaccions no ha de viure en un dimoni.
| Tasca de BiblioTech | Dimoni? | Per què |
|---|---|---|
| Importació del catàleg | No | S'ha de completar o cancel·lar netament |
| Desat de l'estat en sortir | No (i a més és shutdown hook) | Perdre dades és inacceptable |
| Monitor de fils vius | Sí | Purament informatiu |
Neteja periòdica de CacheFitxes |
Sí | Reconstruïble; perdre-la no costa res |
| Enviament d'avisos | No | Un avís a mitges és un avís perdut |
- Dormir:
Thread.sleep i TimeUnit
Thread.sleep i TimeUnitThread.sleep(ms) suspèn el fil actual durant almenys aquell temps. Dues formes equivalents:
Thread.sleep(2000); // milisegons: cal comptar zeros
TimeUnit.SECONDS.sleep(2); // llegible, sense ambigüitat
TimeUnit.MILLISECONDS.sleep(500);
TimeUnit.MINUTES.sleep(5);TimeUnit (de java.util.concurrent) és preferible per llegibilitat: TimeUnit.MINUTES.sleep(5) davant de Thread.sleep(300000). El segon és un error d'un zero esperant a passar. A més TimeUnit és el tipus que fan servir totes les API de concurrència que veuràs a 08-05 (awaitTermination, Future.get, scheduleAtFixedRate), així que convé acostumar-s'hi.
Quatre fets sobre sleep que cal saber:
1. "Almenys", no "exactament". El termini és un mínim. Si el sistema està carregat, el fil pot despertar-se bastant després. No construeixis lògica que depengui de precisió de mil·lisegons.
2. Dormir NO allibera bloquejos. Aquest és el punt crític i es reprendrà a 08-04:
Un fil adormit dins d'un bloc sincronitzat manté el monitor i bloqueja tots els altres. És una recepta de desastre. Object.wait() —que sí que allibera el monitor— és el que es fa servir per coordinar, i és a 08-03.
3. sleep llança InterruptedException. És una excepció comprovada; el compilador t'obliga a tractar-la. Com tractar-la bé és l'apartat següent.
4. sleep(0) no és "no dormir". Pot provocar una cessió al planificador. Si el que vols és suggerir una cessió, existeix Thread.yield() (08-03), i continua sense garantir res.
En aquest mòdul farem servir
sleepper simular latència —escriure al disc, esperar una resposta lenta— perquè les xarxes són el mòdul 9. En producció,sleepdins d'un bucle per "esperar que passi alguna cosa" és un antipatró (busy waiting amb migdiada): el correcte éswait/notify(08-03), unCountDownLatcho unaBlockingQueue(08-05 i 08-06).
- Interrupció: el protocol de cancel·lació
Aquest és l'apartat més important de la lliçó. Java no té forma de matar un fil. Thread.stop() existia i es va eliminar per perillós (08-03). L'única cosa que hi ha és la interrupció: un mecanisme cooperatiu en què un fil demana a un altre que acabi, i l'altre decideix quan i com fer-ho.
9.1 Les tres peces
| Element | Què fa | Efecte sobre el marcador |
|---|---|---|
fil.interrupt() |
Marca l'indicador d'interrupció del fil destí | El posa a true |
fil.isInterrupted() |
Consulta el marcador d'un fil | El deixa com està |
Thread.interrupted() |
Consulta el marcador del fil actual | L'esborra (compte!) |
InterruptedException |
Es llança si el fil estava bloquejat en sleep, wait, join… |
L'esborra en llançar-se |
Les dues files marcades són la causa de gairebé tots els errors d'aquest apartat. Thread.interrupted() és consultar i esborrar; cridar-lo dues vegades seguides retorna true i després false. I una InterruptedException, en llançar-se, neteja el marcador: el fil interromput deixa de semblar interromput justament quan més falta fa saber-ho.
9.2 Què passa segons el que estigui fent el fil
- Si el fil està calculant,
interrupt()només posa el marcador. No passa res més. El fil l'ha de consultar per assabentar-se'n. - Si el fil està bloquejat en
sleep,wait,joino en unaBlockingQueue, es desperta ambInterruptedException. - Si el fil està bloquejat en E/S clàssica (
InputStream.read), la interrupció no el desperta. És una limitació real i molesta. (Els canals de NIO sí que són interrompibles, i els fils virtuals de 10-06 milloren això.)
9.3 L'error d'empassar-se l'excepció
// EL PITJOR CODI DE CONCURRENCIA QUE S'ESCRIU EN JAVA
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// ignorada
}El que acaba de passar: algú va demanar al fil que s'aturés; l'excepció es va llançar, el marcador es va esborrar, i el catch buit se la va empassar. El fil continua com si res i ja no queda ni rastre de la petició. El fil s'ha tornat no cancel·lable.
A BiblioTech, això significa que la importació de cinquanta mil línies no es pot aturar i que l'aplicació no es tanca quan se li demana.
Demostració del problema:
public class InterrupcioEmpassada {
public static void main(String[] args) throws InterruptedException {
Thread dolent = new Thread(() -> {
while (true) {
System.out.println("[dolent] continuo treballant");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// MALAMENT: s'empassa la peticio i esborra el marcador
}
}
}, "fil-immortal");
dolent.start();
Thread.sleep(1000);
System.out.println(">>> main demana la cancellacio");
dolent.interrupt();
Thread.sleep(1000);
System.out.println(">>> continua viu: " + dolent.isAlive());
System.exit(0); // unica forma d'acabar amb ell
}
}Sortida:
[dolent] continuo treballant
[dolent] continuo treballant
[dolent] continuo treballant
>>> main demana la cancellacio
[dolent] continuo treballant
[dolent] continuo treballant
[dolent] continuo treballant
>>> continua viu: true9.4 Les dues respostes correctes
Davant d'una InterruptedException només hi ha dues respostes legítimes:
Resposta A — propagar. Si el teu mètode pot declarar throws InterruptedException, propaga-la. És la millor opció: qui et crida decideix.
public void esperarConfirmacio() throws InterruptedException {
TimeUnit.SECONDS.sleep(2); // l'excepcio puja sola
}Resposta B — restaurar el marcador i acabar. Si no pots propagar —per exemple, dins de run(), la signatura del qual no admet excepcions comprovades—, restaura el marcador i surt ordenadament.
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
processarUnLot();
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
// Restaurar el marcador: el codi de mes amunt (o el pool)
// ha de poder saber que aquest fil va ser interromput.
Thread.currentThread().interrupt();
} finally {
tancarRecursos(); // neteja sempre
}
}El que mai no has de fer: un catch buit, o registrar l'excepció i seguir el bucle com si res.
9.5 El patró complet de tasca cancel·lable
Aquest és l'esquelet que faràs servir una vegada i una altra. Combina les dues formes de detectar la interrupció: el marcador per als trams de càlcul i l'excepció per als trams blocants.
import java.util.concurrent.TimeUnit;
public class TascaCancellable implements Runnable {
private final int totalElements;
public TascaCancellable(int totalElements) {
this.totalElements = totalElements;
}
@Override
public void run() {
String jo = Thread.currentThread().getName();
int processats = 0;
try {
for (int i = 0; i < totalElements; i++) {
// 1) COMPROVAR EL MARCADOR a cada volta.
// Necessari perque la feina de calcul no llanca
// InterruptedException per si sola.
if (Thread.currentThread().isInterrupted()) {
System.out.println("[" + jo + "] cancellacio detectada a l'element " + i);
return; // el finally s'executa igual
}
processarElement(i); // feina real
processats++;
// 2) Punt blocant: aqui la interrupcio arriba
// com a InterruptedException, no com a marcador.
TimeUnit.MILLISECONDS.sleep(20);
}
System.out.println("[" + jo + "] completat");
} catch (InterruptedException e) {
System.out.println("[" + jo + "] interromput mentre esperava");
// 3) RESTAURAR el marcador: no som els amos d'aquesta informacio.
Thread.currentThread().interrupt();
} finally {
// 4) Neteja SEMPRE: s'hi arribi per exit, cancellacio o error.
System.out.println("[" + jo + "] elements processats: " + processats);
}
}
private void processarElement(int i) { /* feina */ }
}I l'ús, amb el patró de cancel·lació amb termini:
public class UsTascaCancellable {
public static void main(String[] args) throws InterruptedException {
Thread f = new Thread(new TascaCancellable(10_000), "bibliotech-importador");
f.start();
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(">>> demanant cancellacio");
f.interrupt();
f.join(2000); // marge perque acabi netament
if (f.isAlive()) {
System.out.println(">>> el fil NO respon a la interrupcio (bug)");
} else {
System.out.println(">>> cancellat netament");
}
}
}Sortida:
>>> demanant cancellacio
[bibliotech-importador] interromput mentre esperava
[bibliotech-importador] elements processats: 23
>>> cancellat netamentEls quatre punts del patró, resumits:
- Comprovar
isInterrupted()a cada volta del bucle de treball. - Capturar
InterruptedExceptionals punts blocants. - Restaurar el marcador amb
Thread.currentThread().interrupt()abans de sortir. - Netejar en un
finally, que s'executa igual en els tres camins.
Freqüència de comprovació. La comprovació ha de ser prou freqüent perquè la cancel·lació es noti (idealment, menys d'un segon de latència) i prou espaiada per no dominar el cost. En un bucle de línies de CSV, comprovar cada línia és correcte:
isInterrupted()és una lectura de camp, costa nanosegons.
- Excepcions en un fil: no van on et penses
Una excepció llançada dins de run() no es propaga al fil que va fer start(). No pot: quan passa, el fil creador és en un altre lloc, o ja ha acabat. Cada fil té la seva pròpia pila i la seva pròpia frontera d'errors.
public class ExcepcioEnFil {
public static void main(String[] args) throws InterruptedException {
Thread f = new Thread(() -> {
System.out.println("[fil] fallare");
throw new IllegalStateException("cataleg corrupte");
}, "bibliotech-importador");
try {
f.start();
f.join();
System.out.println("[main] join() ha tornat SENSE excepcio");
} catch (RuntimeException e) {
System.out.println("[main] aixo no s'imprimeix MAI: " + e);
}
System.out.println("[main] estat del fil: " + f.getState());
}
}Sortida:
[fil] fallare
Exception in thread "bibliotech-importador" java.lang.IllegalStateException: cataleg corrupte
at ExcepcioEnFil.lambda$main$0(ExcepcioEnFil.java:7)
at java.base/java.lang.Thread.run(Thread.java:1583)
[main] join() ha tornat SENSE excepcio
[main] estat del fil: TERMINATEDLlegeix-ho amb atenció: la traça s'imprimeix a la consola, però main no se n'assabenta. join() retorna normalment i l'estat és TERMINATED, exactament igual que si hagués acabat bé. Si main continuava endavant suposant que la importació va funcionar, ara treballa amb un catàleg buit i no ho sap.
Aquest missatge Exception in thread "..." l'imprimeix el gestor d'excepcions no capturades per defecte. El pots substituir, i és exactament el Thread.setDefaultUncaughtExceptionHandler que vas presentar a 06-07 amb GestorGlobal:
import java.util.logging.Level;
import java.util.logging.Logger;
public class GestorDeFil {
private static final Logger LOG = Logger.getLogger("bibliotech");
public static void main(String[] args) throws InterruptedException {
// 1) Gestor GLOBAL: cobreix tots els fils que no tinguin el seu.
// Es el GestorGlobal de 06-07, ara amb sentit ple.
Thread.setDefaultUncaughtExceptionHandler((fil, error) ->
LOG.log(Level.SEVERE,
"Fallada no capturada al fil " + fil.getName(), error));
// 2) Gestor PARTICULAR d'un fil: te prioritat sobre el global.
Thread importador = new Thread(() -> {
throw new IllegalStateException("fitxer de cataleg illegible");
}, "bibliotech-importador");
importador.setUncaughtExceptionHandler((fil, error) -> {
LOG.log(Level.SEVERE, "La importacio ha fallat; el cataleg "
+ "conserva el seu estat anterior", error);
// Aqui es on BiblioTech marcaria la importacio com a fallida
// perque el menu pugui informar l'usuari.
});
importador.start();
importador.join();
// 3) El gestor global en accio, amb un altre fil diferent.
new Thread(() -> { throw new RuntimeException("fallada del monitor"); },
"bibliotech-monitor").start();
}
}Ordre de consulta quan un fil mor per una excepció no capturada:
- El gestor propi del fil (
setUncaughtExceptionHandler), si en té. - El gestor del
ThreadGroup. - El gestor global (
setDefaultUncaughtExceptionHandler). - El comportament per defecte: imprimir la traça a
System.err.
Regla de disseny per a BiblioTech: una tasca de fil hauria de capturar les seves pròpies excepcions de negoci i convertir-les en un resultat —el Resultat que ja tens de 06-07—, deixant el gestor no capturat només com a xarxa de seguretat per a l'imprevist. Aquest és exactament el problema que Future resol netament a 08-05: Future.get() sí que et retorna l'excepció del fil, embolcallada en ExecutionException.
- Passar dades i recollir resultats
Entrada: pel constructor. És la forma correcta, i fa la tasca immutable i segura:
public class TascaImportacio implements Runnable {
// final: no canvien despres de la construccio. Publicacio segura (08-04).
private final Path fitxer;
private final Cataleg cataleg;
public TascaImportacio(Path fitxer, Cataleg cataleg) {
this.fitxer = fitxer;
this.cataleg = cataleg;
}
@Override
public void run() {
// fa servir fitxer i cataleg
}
}Amb lambda, l'entrada es captura, amb la restricció ja coneguda de 04-05: les variables capturades han de ser efectivament finals.
Path fitxer = Path.of("dades/inventari.csv");
Thread f = new Thread(() -> importar(fitxer, cataleg), "bibliotech-importador");
// fitxer = altraCosa; // <-- si descomentes aixo, la lambda NO compilaSortida: el problema. Runnable.run() retorna void i no pot llançar excepcions comprovades. No hi ha cap lloc on posar el resultat. Les solucions manuals són totes insatisfactòries:
// Solucio manual 1: un camp a la tasca.
public class TascaImportacioAmbResultat implements Runnable {
private final Path fitxer;
private volatile Informe resultat; // volatile: visibilitat (08-04)
private volatile Exception error;
public TascaImportacioAmbResultat(Path fitxer) { this.fitxer = fitxer; }
@Override
public void run() {
try {
resultat = new ImportadorCataleg().importar(fitxer);
} catch (Exception e) {
error = e; // cal capturar-la a ma
}
}
public Informe resultat() { return resultat; }
public Exception error() { return error; }
}
// Us:
TascaImportacioAmbResultat tasca =
new TascaImportacioAmbResultat(Path.of("dades/inventari.csv"));
Thread f = new Thread(tasca, "bibliotech-importador");
f.start();
f.join(); // OBLIGATORI abans de llegir
if (tasca.error() != null) {
throw new BiblioTechException("La importacio ha fallat", tasca.error());
}
Informe informe = tasca.resultat();Funciona, però mira tot el que has hagut de fer a mà:
- Declarar els camps
volatileperquè el resultat sigui visible. - Capturar l'excepció tu mateix i desar-la en un altre camp.
- Recordar-te de fer
join()abans de llegir, sense res que t'ho recordi. - Comprovar manualment si hi va haver error abans de fer servir el resultat.
- No tens forma d'esperar amb termini, ni de cancel·lar i saber si es va cancel·lar.
Tot això està resolt a la biblioteca. Callable<Informe> és com Runnable però retorna un valor i pot llançar excepcions comprovades, i Future<Informe> és l'objecte que representa "el resultat que arribarà":
// AVANCAMENT de 08-05, no ho facis servir encara:
Callable<Informe> tasca = () -> new ImportadorCataleg().importar(fitxer);
Future<Informe> futur = executor.submit(tasca);
Informe informe = futur.get(30, TimeUnit.SECONDS); // espera, amb termini,
// i rellanca l'errorEl <Informe> de Future<Informe> és simplement el tipus del resultat que aquell futur lliurarà: un Future<Informe> promet un Informe, un Future<String> promet un String. Els genèrics s'estudien a fons a 10-01; aquí n'hi ha prou de llegir-los així.
- BiblioTech: la importació al seu propi fil, cancel·lable
Ara es junta tot. Aquest és el cas A de 08-01 resolt: la importació deixa de bloquejar el menú, informa del seu progrés i respon a la cancel·lació.
package com.nexussoftware.bibliotech.persistencia;
import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.domini.Material;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Importacio del cataleg executable en un fil a part.
*
* Propietats:
* - Publica el progres (linies llegides) perque el menu el pugui mostrar.
* - Respon a la interrupcio en menys d'una linia de feina.
* - Deixa el cataleg INTACTE si es cancella: construeix una llista a part
* i nomes la bolca al cataleg si la importacio es completa.
*/
public class TascaImportacio implements Runnable {
private static final Logger LOG =
Logger.getLogger(TascaImportacio.class.getName());
private final Path fitxer;
private final Cataleg cataleg;
private final LectorCsv lector = new LectorCsv();
// Estat publicat cap a altres fils. 'volatile' garanteix que el fil
// del menu vegi valors actualitzats; el detall es a 08-04.
private volatile int liniesLlegides = 0;
private volatile int importats = 0;
private volatile int descartats = 0;
private volatile boolean completada = false;
private volatile Exception error = null;
public TascaImportacio(Path fitxer, Cataleg cataleg) {
this.fitxer = fitxer;
this.cataleg = cataleg;
}
@Override
public void run() {
String jo = Thread.currentThread().getName();
long inici = System.nanoTime();
LOG.log(Level.INFO, "[{0}] important {1}", new Object[] { jo, fitxer });
// Llista temporal: el cataleg real no es toca fins al final.
List<Material> nous = new ArrayList<>();
try (BufferedReader br = Files.newBufferedReader(fitxer, StandardCharsets.UTF_8)) {
String linia = br.readLine(); // capcalera
while ((linia = br.readLine()) != null) {
// --- PUNT DE CANCELLACIO ---
// Una comprovacio per linia: cost nanoscopic,
// latencia de cancellacio practicament nulla.
if (Thread.currentThread().isInterrupted()) {
LOG.log(Level.WARNING,
"[{0}] importacio CANCELLADA despres de {1} linies; "
+ "el cataleg conserva el seu contingut anterior",
new Object[] { jo, liniesLlegides });
return; // finally s'executa igual
}
liniesLlegides++;
try {
nous.add(lector.aMaterial(linia));
importats++;
} catch (FormatInvalidException e) {
descartats++; // politica de 06-07: degradar
LOG.log(Level.FINE, "Linia {0} descartada: {1}",
new Object[] { liniesLlegides, e.getMessage() });
}
// Simulacio d'una validacio costosa, perque l'exemple
// duri prou com per poder cancellar-lo.
if (liniesLlegides % 1000 == 0) {
TimeUnit.MILLISECONDS.sleep(50);
}
}
// Nomes si hem arribat fins aqui es publica el resultat.
cataleg.substituirTot(nous);
completada = true;
long ms = (System.nanoTime() - inici) / 1_000_000;
LOG.log(Level.INFO, "[{0}] importacio COMPLETADA: {1} importats, "
+ "{2} descartats, {3} ms",
new Object[] { jo, importats, descartats, ms });
} catch (InterruptedException e) {
LOG.log(Level.WARNING, "[{0}] interrompuda durant una pausa", jo);
Thread.currentThread().interrupt(); // RESTAURAR el marcador
} catch (Exception e) {
// No deixem que mori per excepcio no capturada: la desem
// perque el fil del menu pugui informar l'usuari.
error = e;
LOG.log(Level.SEVERE, "[" + jo + "] importacio fallida", e);
} finally {
LOG.log(Level.INFO, "[{0}] fil d'importacio finalitzat", jo);
}
}
// --- Estat consultable des d'altres fils ---
public int liniesLlegides() { return liniesLlegides; }
public int importats() { return importats; }
public int descartats() { return descartats; }
public boolean completada() { return completada; }
public Exception error() { return error; }
}I l'arrencada des de l'aplicació, amb progrés i cancel·lació per termini:
package com.nexussoftware.bibliotech.presentacio;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
public class ImportacioEnSegonPla {
public static void main(String[] args) throws InterruptedException {
Cataleg cataleg = new Cataleg();
TascaImportacio tasca =
new TascaImportacio(Path.of("dades/inventari.csv"), cataleg);
Thread fil = new Thread(tasca, "bibliotech-importador");
fil.start();
// El fil main NO esta bloquejat: pot pintar progres,
// i a l'aplicacio real atendria el menu.
long limitMs = 10_000;
long inici = System.currentTimeMillis();
while (fil.isAlive()) {
System.out.printf("\r[main] progres: %d linies (%d ok, %d descartades)",
tasca.liniesLlegides(), tasca.importats(), tasca.descartats());
if (System.currentTimeMillis() - inici > limitMs) {
System.out.println("\n[main] termini exhaurit, cancellant");
fil.interrupt();
break;
}
TimeUnit.MILLISECONDS.sleep(200);
}
fil.join(2000); // marge per al tancament net
System.out.println();
if (tasca.completada()) {
System.out.printf("[main] cataleg actualitzat: %d materials%n",
tasca.importats());
} else if (tasca.error() != null) {
System.out.println("[main] la importacio ha fallat: " + tasca.error().getMessage());
System.out.println("[main] el cataleg conserva el seu contingut anterior");
} else {
System.out.println("[main] importacio cancellada; cataleg sense canvis");
}
}
}Sortida en cancel·lar:
[main] progres: 34000 linies (33871 ok, 129 descartades)
[main] termini exhaurit, cancellant
WARNING: [bibliotech-importador] importacio CANCELLADA despres de 34218 linies; el cataleg conserva el seu contingut anterior
INFO: [bibliotech-importador] fil d'importacio finalitzat
[main] importacio cancellada; cataleg sense canvisEls quatre punts de disseny que fan això correcte:
- El catàleg no es toca fins al final. Es construeix una llista a part i només es bolca si la importació es completa. Una cancel·lació a mitges no deixa el catàleg mig vell i mig nou. És la mateixa idea de l'escriptura atòmica de 07-06, aplicada a memòria en lloc de a disc.
- La cancel·lació es comprova una vegada per línia. Cost menyspreable, latència de cancel·lació imperceptible.
- Les excepcions es capturen i es publiquen, no es deixen escapar.
mainpot distingir els tres finals possibles: completada, cancel·lada o fallida. - L'estat publicat és
volatile. Sense això, el fil demainpodria no veure mai el progrés actualitzat. El perquè exacte és a 08-04, i és més subtil del que sembla.
El que encara no està bé, i que 08-05 arregla: es crea un fil a mà per cada importació; no hi ha forma neta d'obtenir el resultat (cal consultar camps); el patró de progrés amb un bucle que dorm 200 ms és tosc; i si hi hagués cinc importacions simultànies, no hi hauria cap control sobre quants fils es creen.
- El que gairebé mai no faràs en producció
Després de tretze apartats ensenyant-te a crear fils, la conclusió honesta: en codi de producció modern gairebé mai no escriuràs new Thread(...).
Els motius ja els coneixes de 08-01: crear un fil costa desenes de microsegons i ~1 MB de pila, i no hi ha cap límit que impedeixi que el codi en creï deu mil. Una fallada típica —"per cada fitxer de la carpeta, un fil"— amb una carpeta de vint mil fitxers tomba la JVM.
El que es fa servir és el pool de fils: un conjunt acotat de fils reutilitzats que consumeixen tasques d'una cua. Això és ExecutorService, i és 08-05. Substitueix:
// Aixo (08-02):
Thread f = new Thread(tasca, "bibliotech-importador");
f.start();
f.join();
// Per aixo (08-05):
Future<Informe> f = executor.submit(tascaQueRetornaInforme);
Informe informe = f.get(30, TimeUnit.SECONDS);Tot i així, tot el d'aquesta lliçó continua sent necessari: els fils del pool són fils normals, cancel(true) d'un Future és un interrupt(), els noms dels fils del pool els posa una ThreadFactory que crea Thread, i quan llegeixis un bolcat a 08-03 veuràs objectes Thread. L'executor no substitueix el coneixement: l'automatitza.
Nota sobre fils virtuals. Java 21 va introduir els fils virtuals (Project Loom): fils gestionats per la JVM, no pel sistema operatiu, el cost de creació dels quals és de nanosegons i la pila dels quals creix dinàmicament al monticle. Amb ells, un milió de fils concurrents és viable i l'argument sencer de "no creïs fils, fes servir un pool" s'inverteix per a tasques d'E/S. Es creen amb
Thread.ofVirtual().start(tasca). No es desenvolupen aquí: són 10-06, quan ja dominis el model clàssic sobre el qual s'apuntalen.
Errors Comuns i Consells
Error 1: cridar run() en lloc de start(). Tot s'executa al fil actual i no hi ha concurrència. Es detecta imprimint Thread.currentThread().getName() dins de la tasca: si diu main, aquí tens la fallada.
Error 2: empassar-se InterruptedException amb un catch buit. Converteix el fil en no cancel·lable i fa que l'aplicació no es pugui tancar. Propaga, o restaura el marcador i acaba. Sense excepcions a aquesta regla.
Error 3: fer servir Thread.interrupted() creient que és isInterrupted(). El primer esborra el marcador. Si el fas servir a la condició del while i a més el consultes al catch, la segona consulta donarà false i trencaràs la teva pròpia lògica de cancel·lació.
Error 4: llegir el resultat sense join(). No és només un problema de temps: sense sincronització, el valor que llegeixis pot ser obsolet encara que el fil ja hagi acabat. join() estableix la relació happens-before que fa el resultat visible.
Error 5: start() dues vegades sobre el mateix Thread. IllegalThreadStateException. Un Thread és d'un sol ús.
Error 6: setDaemon(true) després de start(). IllegalThreadStateException. I encara pitjor: confiar feina important a un dimoni, que s'avorta sense executar el seu finally.
Error 7: creure que una excepció en un fil arribarà al que va fer start(). No hi arriba. join() retorna normalment i l'estat és TERMINATED igual que en el cas d'èxit. Captura i publica l'error, o fes servir Future (08-05).
Error 8: dormir dins d'un bloc sincronitzat. sleep no allibera el monitor. Es veu sencer a 08-04, però apunta-t'ho ja: és una causa habitual d'aplicacions que s'arrosseguen.
Error 9: crear fils en un bucle sobre dades d'entrada. for (Path p : fitxers) new Thread(...).start(); amb vint mil fitxers és un OutOfMemoryError. Pool, sempre.
Consell 1: posa nom a tots els fils. Sense excepció. El cost és una cadena; el benefici és poder diagnosticar en producció.
Consell 2: escriu la tasca com a Runnable, mai com a subclasse de Thread. La podràs provar cridant run() directament en un test, sense arrencar fils, i moure-la a un pool sense tocar-la.
Consell 3: fes cancel·lable tota tasca que duri més d'un segon. El patró de 9.5 és curt i evita una classe sencera de queixes d'usuaris.
Consell 4: no publiquis resultats a mitges. Construeix a part i publica al final, com fa TascaImportacio amb la seva llista temporal. Una cancel·lació no ha de deixar mai l'estat a mitges.
Consell 5: fes servir TimeUnit en lloc de mil·lisegons crus. TimeUnit.MINUTES.sleep(5) no es pot llegir malament; Thread.sleep(300000) sí.
Exercicis
Exercici 1: Les quatre formes i la demostració de start davant de run
Escriu una classe QuatreFormes que executi la mateixa tasca —imprimir tres vegades el nom del fil actual amb una pausa de 100 ms— fent servir les quatre formes de l'apartat 1: subclasse de Thread, classe que implementa Runnable, lambda i classe anònima. Tots els fils han de tenir nom propi (forma-1-thread, forma-2-runnable, forma-3-lambda, forma-4-anonima) i main els ha d'esperar tots amb join(). Afegeix al final una cinquena execució que cridi run() en lloc de start() i comenta a la sortida per què el nom imprès és diferent.
Exercici 2: Un comptador de paraules cancel·lable
Escriu ComptadorParaules implements Runnable que rebi un Path al constructor i compti les paraules del fitxer línia a línia. La tasca ha de:
- Comprovar la interrupció cada 100 línies.
- Publicar el progrés (línies processades i paraules comptades) en camps
volatileconsultables des de fora. - Restaurar el marcador d'interrupció si és interrompuda durant una espera.
- Registrar al logger, en un
finally, el resultat parcial i el temps transcorregut mesurat ambSystem.nanoTime().
Escriu també un main que la llanci sobre un fitxer gran, mostri el progrés cada 300 ms i la cancel·li als 2 segons, informant de si va acabar o va ser cancel·lada.
Exercici 3: Recol·lector de resultats de diversos fils
Escriu RecollidorAvisos que llanci quatre fils, cadascun encarregat d'"enviar" un lot de 25 avisos de BiblioTech (simula cada enviament amb TimeUnit.MILLISECONDS.sleep(20)). Cada fil ha de desar a la seva pròpia tasca quants avisos va enviar amb èxit i quants van fallar (simula una fallada quan el número d'avís sigui múltiple de 7, llançant i capturant una RuntimeException). main ha d'esperar els quatre amb join(), sumar els resultats i imprimir un resum, a més de mesurar el temps total amb System.nanoTime() i comparar-lo amb el temps que hauria trigat la versió seqüencial (100 × 20 ms = 2000 ms).
Instal·la a més un setUncaughtExceptionHandler global que registri qualsevol fallada imprevista, i comprova que no es dispara.
Solucions
Solució a l'Exercici 1
import java.util.concurrent.TimeUnit;
public class QuatreFormes {
/** Cos comu de la tasca, perque la comparacio sigui justa. */
static void feina() {
for (int i = 1; i <= 3; i++) {
System.out.printf(" [%s] pas %d%n",
Thread.currentThread().getName(), i);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
// --- FORMA 1: estendre Thread ---
// La classe ES un fil: gasta l'unica herencia disponible.
static class FilPropi extends Thread {
FilPropi() { super("forma-1-thread"); }
@Override public void run() { feina(); }
}
// --- FORMA 2: implementar Runnable ---
// La classe descriu una TASCA; qui l'executa es decideix fora.
static class TascaPropia implements Runnable {
@Override public void run() { feina(); }
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== FORMA 1: estendre Thread ===");
Thread f1 = new FilPropi();
f1.start();
f1.join();
System.out.println("=== FORMA 2: implementar Runnable ===");
Thread f2 = new Thread(new TascaPropia(), "forma-2-runnable");
f2.start();
f2.join();
System.out.println("=== FORMA 3: lambda ===");
// Runnable es una interficie funcional (04-06): un sol metode run().
Thread f3 = new Thread(() -> feina(), "forma-3-lambda");
f3.start();
f3.join();
System.out.println("=== FORMA 4: classe anonima ===");
Thread f4 = new Thread(new Runnable() {
@Override public void run() { feina(); }
}, "forma-4-anonima");
f4.start();
f4.join();
System.out.println("=== ERROR CLASSIC: run() en lloc de start() ===");
Thread f5 = new Thread(() -> feina(), "forma-5-mai-arrenca");
f5.run(); // NO crea fil: executa aqui mateix
System.out.println(" estat de f5: " + f5.getState()
+ " (NEW: no va arrencar mai)");
}
}Sortida (fragment final):
=== ERROR CLASSIC: run() en lloc de start() ===
[main] pas 1
[main] pas 2
[main] pas 3
estat de f5: NEW (NEW: no va arrencar mai)La demostració està en el nom imprès: diu main, no forma-5-mai-arrenca. El codi de la tasca es va executar, però al fil equivocat. I l'estat NEW confirma que aquell objecte Thread no va arribar mai a existir com a fil del sistema operatiu.
Solució a l'Exercici 2
package com.nexussoftware.bibliotech.persistencia;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ComptadorParaules implements Runnable {
private static final Logger LOG =
Logger.getLogger(ComptadorParaules.class.getName());
private final Path fitxer;
// Estat publicat cap al fil que observa. 'volatile' assegura
// que les escriptures d'aquest fil siguin visibles des de fora (08-04).
private volatile long liniesProcessades = 0;
private volatile long paraulesComptades = 0;
private volatile boolean completat = false;
private volatile boolean cancellat = false;
public ComptadorParaules(Path fitxer) {
this.fitxer = fitxer;
}
@Override
public void run() {
String jo = Thread.currentThread().getName();
long inici = System.nanoTime();
try (BufferedReader br = Files.newBufferedReader(fitxer, StandardCharsets.UTF_8)) {
String linia;
while ((linia = br.readLine()) != null) {
// 1) Comprovacio periodica del marcador. Cada 100 linies
// n'hi ha prou: la latencia de cancellacio sera de
// microsegons i el cost de la comprovacio, nul.
if (liniesProcessades % 100 == 0
&& Thread.currentThread().isInterrupted()) {
cancellat = true;
LOG.log(Level.WARNING, "[{0}] cancellat a la linia {1}",
new Object[] { jo, liniesProcessades });
return; // el finally s'executa igual
}
liniesProcessades++;
// Comptar paraules: separar per espais en blanc.
// El trim evita que una linia buida compti com una paraula.
String neta = linia.trim();
if (!neta.isEmpty()) {
paraulesComptades += neta.split("\\s+").length;
}
// 2) Simulacio de feina pesada perque es pugui cancellar.
if (liniesProcessades % 500 == 0) {
TimeUnit.MILLISECONDS.sleep(30);
}
}
completat = true;
} catch (InterruptedException e) {
// 3) RESTAURAR el marcador: no som els amos d'aquesta informacio.
cancellat = true;
Thread.currentThread().interrupt();
LOG.log(Level.WARNING, "[{0}] interromput durant una pausa", jo);
} catch (IOException e) {
LOG.log(Level.SEVERE, "[" + jo + "] error llegint " + fitxer, e);
} finally {
// 4) Informe SEMPRE, s'hi arribi com s'hi arribi.
long ms = (System.nanoTime() - inici) / 1_000_000;
LOG.log(Level.INFO,
"[{0}] fi ({1}): {2} linies, {3} paraules, {4} ms",
new Object[] { jo,
completat ? "completat" : (cancellat ? "cancellat" : "error"),
liniesProcessades, paraulesComptades, ms });
}
}
public long liniesProcessades() { return liniesProcessades; }
public long paraulesComptades() { return paraulesComptades; }
public boolean completat() { return completat; }
public boolean cancellat() { return cancellat; }
// --- Demostracio ---
public static void main(String[] args) throws InterruptedException {
ComptadorParaules tasca =
new ComptadorParaules(Path.of("dades/cataleg-complet.txt"));
Thread fil = new Thread(tasca, "bibliotech-comptador");
long inici = System.nanoTime();
fil.start();
while (fil.isAlive()) {
System.out.printf("[main] %d linies, %d paraules%n",
tasca.liniesProcessades(), tasca.paraulesComptades());
if ((System.nanoTime() - inici) > 2_000_000_000L) { // 2 s
System.out.println("[main] termini exhaurit -> interrupt()");
fil.interrupt();
break;
}
TimeUnit.MILLISECONDS.sleep(300);
}
fil.join(1000);
if (tasca.completat()) {
System.out.printf("[main] COMPLETAT: %d paraules en %d linies%n",
tasca.paraulesComptades(), tasca.liniesProcessades());
} else if (tasca.cancellat()) {
System.out.printf("[main] CANCELLAT despres de %d linies (resultat parcial: %d paraules)%n",
tasca.liniesProcessades(), tasca.paraulesComptades());
} else {
System.out.println("[main] acabat amb error; consulta el log");
}
}
}Detalls que mereixen atenció:
- La comprovació del marcador va condicionada a
liniesProcessades % 100 == 0per no pagar-la a cada línia; en realitatisInterrupted()és tan barat que es podria fer sempre, però el patró condicionat és el que faràs servir quan la comprovació sigui cara (per exemple, consultar un rellotge). completaticancellatsón camps diferents a propòsit: permeten distingir tres finals (èxit, cancel·lació, error), i el missatge a l'usuari és diferent en cada cas.- El
try-with-resourcestanca elBufferedReaderfins i tot en ferreturndins del bucle. És exactament la garantia de 06-06 i per això no cal tancar-lo a mà alfinally.
Solució a l'Exercici 3
package com.nexussoftware.bibliotech.servei;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class RecollidorAvisos {
private static final Logger LOG =
Logger.getLogger(RecollidorAvisos.class.getName());
/** Tasca que envia un lot d'avisos i publica el seu resultat. */
static class LotAvisos implements Runnable {
private final int des;
private final int fins; // exclusiu
private volatile int enviats = 0;
private volatile int fallits = 0;
LotAvisos(int des, int fins) {
this.des = des;
this.fins = fins;
}
@Override
public void run() {
String jo = Thread.currentThread().getName();
try {
for (int n = des; n < fins; n++) {
if (Thread.currentThread().isInterrupted()) {
LOG.log(Level.WARNING, "[{0}] cancellat a l'avis {1}",
new Object[] { jo, n });
return;
}
try {
enviarAvis(n);
enviats++;
} catch (RuntimeException e) {
// Politica de 06-07: un avis fallit no avorta el lot.
fallits++;
LOG.log(Level.FINE, "[{0}] avis {1} fallit: {2}",
new Object[] { jo, n, e.getMessage() });
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/** Simula l'enviament: 20 ms de latencia i fallada als multiples de 7. */
private void enviarAvis(int n) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(20);
if (n % 7 == 0) {
throw new RuntimeException("destinatari " + n + " sense adreca");
}
}
int enviats() { return enviats; }
int fallits() { return fallits; }
}
public static void main(String[] args) throws InterruptedException {
// Xarxa de seguretat: qualsevol fallada imprevista queda registrada
// en lloc d'imprimir-se a System.err i perdre's (06-07).
Thread.setDefaultUncaughtExceptionHandler((fil, error) ->
LOG.log(Level.SEVERE,
"Fallada NO capturada al fil " + fil.getName(), error));
final int LOTS = 4;
final int PER_LOT = 25;
LotAvisos[] tasques = new LotAvisos[LOTS];
Thread[] fils = new Thread[LOTS];
long inici = System.nanoTime();
// 1) Crear i arrencar. start() retorna de seguida: els quatre
// lots avancen alhora.
for (int i = 0; i < LOTS; i++) {
tasques[i] = new LotAvisos(i * PER_LOT, (i + 1) * PER_LOT);
fils[i] = new Thread(tasques[i], "bibliotech-avisos-" + (i + 1));
fils[i].start();
}
// 2) Esperar-los tots. El join() de cada fil garanteix a mes
// que les seves escriptures siguin visibles des de main (08-04).
for (Thread f : fils) {
f.join();
}
long ms = (System.nanoTime() - inici) / 1_000_000;
// 3) Agregar resultats.
int totalEnviats = 0;
int totalFallits = 0;
System.out.println("=== RESULTAT PER LOT ===");
for (int i = 0; i < LOTS; i++) {
System.out.printf(" %-24s enviats=%2d fallits=%d%n",
fils[i].getName(), tasques[i].enviats(), tasques[i].fallits());
totalEnviats += tasques[i].enviats();
totalFallits += tasques[i].fallits();
}
long sequencialMs = (long) LOTS * PER_LOT * 20;
System.out.println();
System.out.println("=== RESUM ===");
System.out.println("Avisos processats : " + (totalEnviats + totalFallits));
System.out.println("Enviats : " + totalEnviats);
System.out.println("Fallits : " + totalFallits);
System.out.println("Temps real : " + ms + " ms");
System.out.println("Temps sequencial : " + sequencialMs + " ms (estimat)");
System.out.printf ("Acceleracio : %.2fx%n", (double) sequencialMs / ms);
}
}Sortida orientativa:
=== RESULTAT PER LOT ===
bibliotech-avisos-1 enviats=21 fallits=4
bibliotech-avisos-2 enviats=21 fallits=4
bibliotech-avisos-3 enviats=22 fallits=3
bibliotech-avisos-4 enviats=22 fallits=3
=== RESUM ===
Avisos processats : 100
Enviats : 86
Fallits : 14
Temps real : 528 ms
Temps sequencial : 2000 ms (estimat)
Acceleracio : 3.79xTres lliçons d'aquest resultat:
- L'acceleració és de gairebé 4×, no de 4×. Hi falta el cost de crear els fils, el d'arrencar-los esglaonadament i el fet que els lots no triguen exactament el mateix. És el cas B de 08-01 resolt a mitges: els 100 avisos passen de 2 s a 0,5 s.
- Les fallades individuals no tomben el lot. Cada
enviarAvisva al seu propitry; la política de 06-07 —degradar, no avortar— continua vigent i ara també dins d'un fil. - El gestor global no es dispara. Aquest és l'objectiu: si apareix a la sortida, significa que una tasca va deixar escapar una excepció, i això és un bug de la tasca, no una situació normal.
I la limitació evident: i si en lloc de 4 lots fossin 200 avisos individuals? Crearies 200 fils. Cadascun costa ~1 MB de pila i desenes de microsegons, i per a una tasca de 20 ms això és un malbaratament. La resposta és a 08-05.
Conclusió
Ja saps crear fils i, més important, saps com es manegen bé.
Les quatre formes de crear un fil —estendre Thread, implementar Runnable, lambda i classe anònima— acaben en el mateix, però només dues són recomanables: Runnable com a classe quan la tasca té estat i mereix proves pròpies, i lambda per a tota la resta. La raó per evitar extends Thread no és d'estil: gasta l'única herència disponible, confon la tasca amb el mecanisme —herència davant de composició, un altre cop 03-05— i, sobretot, deixa fora del teu abast els pools, els programadors periòdics i la prova unitària que crida run() sense arrencar cap fil. Escriu tasques, no fils.
Coneixes l'error que no dona error: run() executa al fil actual, start() en crea un de nou. Es detecta en un segon imprimint Thread.currentThread().getName() dins de la tasca, i el getState() de l'objecte ho confirma: un Thread sobre el qual només es va cridar run() es queda a NEW per sempre. I saps que un Thread és d'un sol ús: un segon start() llança IllegalThreadStateException.
Domines el cicle bàsic —crear, start(), join()— amb els seus dos matisos importants: join() no és només esperar, és també fer visibles les escriptures del fil que acaba, i join(ms) no diu si va acabar o si es va exhaurir el termini, així que cal preguntar-ho a isAlive(). Saps per què posar nom als fils és una necessitat operativa i no un adorn, amb la convenció bibliotech-<funcio>; per què les prioritats són un suggeriment que el sistema operatiu pot ignorar i que mai no ha de formar part d'un argument de correcció; i quan un fil ha de ser dimoni —només si perdre la seva feina a mitja execució és acceptable—, amb setDaemon(true) sempre abans de start().
I tens el més valuós de la lliçó: el protocol d'interrupció. Saps que Java no pot matar un fil i que l'única cosa que existeix és una petició cooperativa; que interrupt() posa un marcador, isInterrupted() el llegeix, Thread.interrupted() el llegeix i l'esborra, i que una InterruptedException, en llançar-se, esborra el marcador justament quan més falta fa. D'aquí les dues úniques respostes legítimes: propagar l'excepció, o restaurar el marcador i acabar. Un catch (InterruptedException e) { } buit converteix un fil en no cancel·lable i una aplicació en una cosa que no es pot tancar; és, amb diferència, el pitjor error freqüent de la concurrència en Java.
Saps també que una excepció llançada en un fil no arriba al que va fer start(): join() retorna amb normalitat i l'estat queda a TERMINATED igual que si tot hagués anat bé. Per això una tasca ha de capturar i publicar el seu error, i per això setUncaughtExceptionHandler —el GestorGlobal de 06-07— és la xarxa de seguretat i no l'estratègia. I saps passar dades pel constructor amb camps final, i per què Runnable no pot retornar res, amb Callable i Future<Informe> esperant-te a la propera parada.
BiblioTech ja no bloqueja el menú. TascaImportacio corre a bibliotech-importador, publica el seu progrés en camps volatile, comprova la cancel·lació una vegada per línia, distingeix els tres finals possibles —completada, cancel·lada, fallida— i, el més important de tot, construeix una llista a part i només la bolca al catàleg si acaba, de manera que una cancel·lació a mitges no deixa el catàleg mig vell i mig nou. És l'escriptura atòmica de 07-06 portada a la memòria.
Però el codi deixa tres coses sense resoldre, i les tres són visibles a simple vista: crees un fil a mà per cada tasca, i amb 200 avisos això serien 200 fils i 200 MB de piles; no hi ha forma neta de recollir un resultat, sinó camps volatile i una convenció implícita de "fes join() abans de llegir"; i el progrés es consulta amb un bucle que dorm 200 ms, que és sondeig, no coordinació.
A la propera lliçó, Cicle de Vida d'un Fil, baixes un nivell més abans de pujar-ne dos. Veuràs els sis estats de Thread.State i quina operació exacta provoca cada transició; la diferència entre BLOCKED —esperant un monitor— i WAITING —esperant un senyal—, que és el que fa llegible un bolcat de fils; el mecanisme de coordinació de baix nivell wait/notify/notifyAll, amb el bucle while obligatori i per què fer servir un if allà és un error; per què stop, suspend i resume es van retirar de l'API; i —el més útil a la pràctica— com obtenir i llegir un bolcat de fils amb jstack per trobar un fil bloquejat o un interbloqueig que la JVM ha detectat per tu.
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
