Aquesta és la lliçó central del mòdul. Tot l'anterior va ser preparació: saps què és un fil, saps crear-lo, cancel·lar-lo i observar en quin estat és. Ara toca resoldre el problema que dona sentit a tot el tema, i que continua obert des de 08-01: dos fils que sumen un a un comptador dos milions de vegades i n'obtenen un milió tres-cents mil.
La lliçó té dues meitats. La primera és teoria que no et pots saltar: per què comptador++ no és una operació sinó tres, què és exactament una operació atòmica, i —el més profund del mòdul— el model de memòria de Java, que explica per què un fil pot escriure un valor i un altre no veure'l mai, encara que passin minuts. Aquesta part és incòmoda perquè contradiu la intuïció que la memòria és un lloc on s'escriu i es llegeix; però sense ella, volatile i synchronized són màgia que s'aplica per superstició.
La segona meitat és la caixa d'eines: volatile per a visibilitat, synchronized per a exclusió mútua, ReentrantLock quan synchronized no hi arriba, ReadWriteLock quan es llegeix molt més del que s'escriu, i les dues estratègies que guanyen a totes: immutabilitat i confinament. Entremig, l'interbloqueig reproduïble i les seves solucions.
En acabar, Cataleg i RegistrePrestecs seran segurs per a diversos fils, i el cas C de 08-01 —Marta Ruiz i Diego Alonso registrant préstecs alhora— deixarà de ser una amenaça.
Com estudiar aquesta lliçó. És llarga i densa a propòsit: és la que sosté la resta del mòdul i bona part de la teva carrera escrivint serveis. Executa els exemples que fallen. Especialment el de l'apartat 5, el marcador d'aturada que sense
volatileno es veu mai: la primera vegada que el vegis penjar-se entendràs el model de memòria millor que amb deu pàgines de text.
Contingut
- La condició de cursa al nivell del bytecode
- Què és una operació atòmica
- El model de memòria de Java
- La relació happens-before
volatile: el que garanteixvolatile: el que NO garanteixsynchronized: el monitor intrínsec- Les formes de
synchronized - Què sincronitzar i què no
- Interbloqueig: l'exemple reproduïble
- Les solucions a l'interbloqueig
- Inanició i livelock
ReentrantLockdavant desynchronizedCondition: substitut dewait/notifyReadWriteLock: llegir molt, escriure poc- Publicació segura i immutabilitat
- Confinament: la millor sincronització és no compartir
- BiblioTech: un catàleg segur per a diversos fils
- Errors Comuns i Consells
- Exercicis
- La condició de cursa al nivell del bytecode
comptador++ sembla una operació. No ho és. Compila-ho i mira-ho:
public void incrementar();
Code:
0: aload_0 // apilar la referencia 'this'
1: dup // duplicar-la (cal dues vegades)
2: getfield #7 // LLEGIR el camp 'valor' <-- operacio 1
5: iconst_1 // apilar la constant 1
6: iadd // SUMAR <-- operacio 2
7: putfield #7 // ESCRIURE el camp 'valor' <-- operacio 3
10: returnTres operacions separades: llegir, sumar, escriure. Entre qualsevol d'elles el planificador pot treure el fil i posar-ne un altre. Aquest forat és la condició de cursa.
L'entrellaçat que perd un increment, pas a pas, partint de valor = 10:
sequenceDiagram
participant A as sumador-1
participant M as memòria (valor)
participant B as sumador-2
Note over M: valor = 10
A->>M: getfield -> llegeix 10
Note over A: registre A = 10
B->>M: getfield -> llegeix 10
Note over B: registre B = 10
Note over A: iadd -> A = 11
Note over B: iadd -> B = 11
A->>M: putfield 11
Note over M: valor = 11
B->>M: putfield 11
Note over M: valor = 11 (una altra vegada!)
Note over A,B: Dos increments, el valor només ha pujat 1.<br/>Un increment PERDUT.
Els dos fils van llegir 10, tots dos van calcular 11, tots dos van escriure 11. S'han executat dos increments i el comptador ha pujat un. Repeteix això un milió de vegades amb entrellaçats aleatoris i tens exactament la sortida de 08-01: centenars de milers d'increments perduts.
Aquest patró té nom: llegir-modificar-escriure (read-modify-write). Apareix en moltes més formes de les que sembla, i totes són insegures sense protecció:
valor++; // llegir, sumar, escriure
valor--; // idem
valor += 5; // idem
saldo = saldo - quantitat; // idem
if (!mapa.containsKey(k)) { // COMPROVAR...
mapa.put(k, v); // ...DESPRES ACTUAR: un altre fil pot colar-se enmig
}
if (instancia == null) { // el singleton mandros classic
instancia = new Servei();
}
llista.add(llista.size(), x); // llegir mida, despres fer-la servirLes dues últimes famílies —comprovar-després-actuar i llegir-modificar-escriure— són les dues formes canòniques de condició de cursa. Si veus qualsevol d'elles sobre estat compartit, hi ha un bug.
- Què és una operació atòmica
Una operació és atòmica si, des del punt de vista dels altres fils, passa sencera o no passa: no hi ha estat intermedi observable.
En Java són atòmiques:
- La lectura i l'escriptura de qualsevol variable de tipus primitiu llevat de
longidouble, i de qualsevol referència. - La lectura i l'escriptura d'un
longodoubledeclaratvolatile. - Les operacions de les classes de
java.util.concurrent.atomic(08-06).
No són atòmiques:
valor++,valor--,valor += n(són tres operacions).- Qualsevol seqüència de dues o més operacions que s'hagi de veure com una de sola.
- La lectura o escriptura d'un
long/doublenovolatile.
El cas curiós de long i double. L'especificació permet que la JVM implementi l'escriptura d'un valor de 64 bits com dues escriptures de 32 bits. En una JVM de 32 bits això passava de veritat, i produïa un fenomen inquietant: un fil podia llegir un long amb la meitat alta d'una escriptura i la meitat baixa d'una altra, obtenint un valor que cap fil no va escriure mai. Se'n deia word tearing.
public class LongPartit {
// Sense volatile, en una JVM de 32 bits, un lector podia veure
// 0x00000000FFFFFFFF: meitat d'un valor i meitat d'un altre.
private long comptadorPrestecs = 0;
// Amb volatile, l'escriptura de 64 bits es atomica per especificacio.
private volatile long comptadorSegur = 0;
}A les JVM de 64 bits actuals això ja no passa a la pràctica, però l'especificació ho continua permetent, així que el codi correcte no en depèn: si comparteixes un long o un double, declara'l volatile o protegeix-lo.
El punt que cal endur-se: l'atomicitat d'una operació individual no n'hi ha prou. Encara que cada lectura i cada escriptura de valor siguin atòmiques, valor++ continua sent incorrecte perquè són tres operacions atòmiques separades. L'atomicitat que necessites és la de l'operació composta, i aquesta l'has de construir.
- El model de memòria de Java
Aquí arriba la part que contradiu la intuïció. Fins ara has suposat que la memòria és una llibreta compartida: un fil escriu una línia i un altre la llegeix. Aquesta imatge és falsa en qualsevol processador dels últims trenta anys.
3.1 Les tres raons per les quals la memòria no es comporta com esperes
Raó 1: cada nucli té les seves pròpies memòries cau.
Accedir a la memòria principal costa uns centenars de cicles. Accedir a la memòria cau L1 del nucli en costa uns pocs. Per això cada nucli manté còpies locals de les dades que fa servir. Quan el nucli 1 escriu comptador = 5, aquest 5 pot quedar-se a la seva memòria cau L1 durant un temps indeterminat abans d'arribar a un nivell visible per al nucli 2.
flowchart TB
subgraph CPU["Processador"]
direction LR
subgraph N1["Nucli 1 - fil A"]
R1["Registres"] --- L11["Memòria cau L1<br/>comptador = 5"]
end
subgraph N2["Nucli 2 - fil B"]
R2["Registres"] --- L12["Memòria cau L1<br/>comptador = 0"]
end
end
L11 --- L2["Memòria cau L2/L3 compartida"]
L12 --- L2
L2 --- RAM["Memòria principal<br/>comptador = 0"]
En aquest dibuix, el fil A ha escrit 5 i el fil B continua llegint 0. Tots dos tenen raó segons la seva pròpia memòria cau. I no hi ha cap garantia temporal de quan —ni de si— B veurà el 5.
Raó 2: el compilador reordena instruccions.
Tant javac com, sobretot, el compilador JIT de la JVM reordenen lliurement el codi mentre el resultat sigui el mateix per a un sol fil. Aquesta clàusula és la clau: la garantia s'anomena as-if-serial i només aplica dins d'un fil.
// El que escrius:
dades = carregarCataleg(); // (1)
llest = true; // (2)
// El que el compilador pot generar, perque per a AQUEST fil
// el resultat es indistingible:
llest = true; // (2)
dades = carregarCataleg(); // (1)Per a un altre fil que faci if (llest) usar(dades), aquesta reordenació és catastròfica: pot veure llest == true amb dades == null.
Raó 3: la CPU també reordena.
Els processadors moderns executen fora d'ordre i tenen buffers d'escriptura. Encara que el compilador emeti les instruccions en ordre, el maquinari les pot fer efectives en un altre. En x86 el model és relativament fort; en ARM —el teu mòbil, molts servidors— és molt més feble i les reordenacions s'observen amb facilitat.
3.2 La conseqüència: dos fils poden veure històries diferents
Sense sincronització, no hi ha cap garantia que un fil vegi les escriptures d'un altre, ni en el mateix ordre, ni mai. No és que trigui: pot no veure-les mai.
Aquest programa ho demostra, i és l'exemple més impactant del mòdul:
public class MarcadorSenseVolatile {
// SENSE volatile. Aqui hi ha el problema.
private static boolean aturat = false;
public static void main(String[] args) throws InterruptedException {
Thread treballador = new Thread(() -> {
long voltes = 0;
// El JIT pot transformar aixo en 'while (true)' perque
// dins d'AQUEST fil 'aturat' no canvia mai: es una
// optimitzacio legal anomenada elevacio (hoisting).
while (!aturat) {
voltes++;
}
System.out.println("[treballador] aturat despres de " + voltes + " voltes");
}, "bibliotech-treballador");
treballador.start();
Thread.sleep(1000);
System.out.println("[main] escrivint aturat = true");
aturat = true;
treballador.join(3000);
if (treballador.isAlive()) {
System.out.println("[main] EL TREBALLADOR NO SE N'HA ASSABENTAT. Continua viu.");
System.exit(1);
}
}
}Sortida típica amb el JIT en marxa (compila amb javac i executa normalment, sense depurador):
El fil treballador no s'atura mai. main va escriure true i el treballador va continuar llegint false indefinidament. No és un retard de microsegons: és per sempre.
Per què passa: el compilador JIT veu que dins del bucle ningú no modifica aturat, així que treu la lectura fora del bucle —optimització estàndard i perfectament legal segons la garantia as-if-serial— i el converteix en l'equivalent de:
Si en executar-lo sí que s'atura, prova amb més temps d'escalfament (el JIT triga uns quants milers d'iteracions a compilar), o afegeix
-XX:+PrintCompilationper veure quan compila el mètode. En un depurador o amb-Xint(només intèrpret) gairebé sempre s'atura, perquè l'intèrpret no aplica aquesta optimització. Aquest és precisament el perill: el bug desapareix sota el depurador.
Canvia boolean aturat per volatile boolean aturat i executa de nou:
S'atura immediatament. Aquesta paraula clau és la diferència entre un programa correcte i un que es penja.
- La relació happens-before
El model de memòria de Java (JSR 133, incorporat a Java 5) no descriu memòries cau ni reordenacions: defineix una relació d'ordre parcial anomenada happens-before entre accions, i una única garantia:
Si l'acció A happens-before l'acció B, llavors els efectes d'A són visibles per a B, i B veu A com a ocorreguda abans.
I la seva contrapositiva, que és la que faràs servir a la pràctica:
Si dues accions no estan relacionades per happens-before i almenys una escriu, hi ha una cursa de dades i no hi ha cap garantia sobre el que s'observa.
Les regles que estableixen happens-before —memoritza-les, són la caixa d'eines sencera:
| Regla | Estableix que… |
|---|---|
| Ordre del programa | Dins d'un mateix fil, cada acció happens-before les següents al codi |
| Monitor | Deixar anar un monitor happens-before qualsevol adquisició posterior del mateix monitor |
volatile |
Escriure una variable volatile happens-before qualsevol lectura posterior d'aquella mateixa variable |
Thread.start() |
Tot el fet abans de start() happens-before la primera acció del fil nou |
Thread.join() |
Tot el fet pel fil happens-before el retorn de join() |
Camps final |
La inicialització d'un camp final al constructor happens-before que un altre fil vegi l'objecte correctament construït |
| Transitivitat | Si A hb B i B hb C, llavors A hb C |
Utilitats de j.u.c. |
Posar en una cua concurrent hb treure'n; countDown hb await; etc. (08-05, 08-06) |
Aplicat als casos que ja coneixes:
// REGLA start(): main escriu, el fil nou ho veu garantit.
cataleg.carregar(); // A
Thread f = new Thread(tasca);
f.start(); // A happens-before tot el del fil nou
// dins de 'tasca': veu el cataleg carregat, segur.
// REGLA join(): el fil escriu, main ho veu garantit.
f.start();
f.join(); // tot el del fil hb el retorn de join
System.out.println(tasca.resultat()); // valor correcte garantit
// REGLA monitor: transferencia de visibilitat entre seccions critiques.
synchronized (pany) { x = 42; } // deixar anar hb...
// ... un altre fil:
synchronized (pany) { llegir(x); } // ...adquirir: veu x == 42
// REGLA volatile + transitivitat: l'idioma de la "publicacio segura".
dades = carregarCataleg(); // (1) escriptura normal
llest = true; // (2) escriptura VOLATILE
// ... un altre fil:
if (llest) { // (3) lectura VOLATILE
usar(dades); // (4) veu dades correctament. Per que?
}
// (1) hb (2) per ordre del programa;
// (2) hb (3) per la regla volatile;
// (3) hb (4) per ordre del programa;
// per transitivitat: (1) hb (4). L'acces a 'dades' es segur
// ENCARA QUE 'dades' no sigui volatile.Aquest últim cas és important i sorprèn: una única variable volatile pot fer visibles escriptures normals que la precedeixen. És el que s'anomena publicació segura, i és la base del patró de l'apartat 16.
La forma pràctica de fer servir tot això, sense memoritzar l'especificació: cada vegada que dos fils toquin la mateixa dada i almenys un escrigui, pregunta't "quina regla happens-before connecta aquestes dues accions?". Si no saps contestar, no n'hi ha cap, i tens una cursa de dades.
volatile: el que garanteix
volatile: el que garanteixvolatile és el mecanisme de sincronització més lleuger de Java. Aporta dues garanties, ni una més:
Garantia 1: visibilitat. Una escriptura volatile es fa visible immediatament a qualsevol fil que llegeixi després aquella variable. A la pràctica, la JVM emet barreres de memòria: l'escriptura buida el buffer cap a memòria compartida i la lectura invalida la còpia en memòria cau.
Garantia 2: no reordenació. Les lectures i escriptures volatile no es reordenen entre si, i actuen com a barrera per a les que les envolten: res que estigui abans d'una escriptura volatile no es pot moure després, i res que estigui després d'una lectura volatile no es pot moure abans.
A més, com a efecte derivat: les lectures i escriptures de long/double volatile són atòmiques (adeu al word tearing).
El cas d'ús canònic, i pràcticament l'únic que necessitaràs: el marcador d'estat escrit per un fil i llegit per uns altres.
package com.nexussoftware.bibliotech.persistencia;
/**
* Servei d'importacio amb aturada neta mitjancant marcador volatile.
* Es el complement de la interrupcio de 08-02: el marcador propi
* permet una aturada ordenada sense dependre de l'estat d'interrupcio.
*/
public class ServeiImportacio implements Runnable {
// volatile: escrit pel fil del menu, llegit pel fil treballador.
// SENSE volatile, el treballador podria no assabentar-se'n MAI (apartat 3.2).
private volatile boolean aturat = false;
// Progres publicat cap al fil del menu. Nomes l'escriu AQUEST fil,
// aixi que no hi ha llegir-modificar-escriure concurrent: volatile n'hi ha prou.
private volatile int liniesProcessades = 0;
public void aturar() {
aturat = true;
}
@Override
public void run() {
while (!aturat && !Thread.currentThread().isInterrupted()) {
processarLot();
liniesProcessades += 1000; // segur: UN sol escriptor
}
}
public int liniesProcessades() { return liniesProcessades; }
private void processarLot() { /* ... */ }
}Fixa't en el comentari de liniesProcessades: += 1000 és llegir-modificar-escriure i en general seria insegur… però només hi ha un escriptor. Amb un únic fil escrivint, no hi ha entrellaçat possible entre lectura i escriptura d'aquest fil amb si mateix, i volatile dona als lectors la visibilitat que necessiten. Aquest raonament és vàlid i freqüent; el que l'invalidaria és un segon escriptor.
Quan volatile és suficient, en resum:
- L'escriptura no depèn del valor actual (o hi ha un únic escriptor).
- No forma part d'un invariant juntament amb altres variables.
- No cal bloqueig per cap altre motiu.
Si es compleixen les tres, volatile és l'opció correcta i és molt més barata que un pany: no bloqueja, no crea contenció i no pot provocar interbloqueig.
volatile: el que NO garanteix
volatile: el que NO garanteixAquí hi ha la confusió més estesa del tema, i convé destruir-la amb una demostració.
volatileNO fa atòmiques les operacions compostes.
public class ComptadorVolatileEncaraTrencat {
// volatile SI que garanteix que els dos fils vegin el valor mes recent.
// volatile NO garanteix que 'valor++' sigui indivisible.
private volatile int valor = 0;
public void incrementar() {
valor++; // continua sent LLEGIR, SUMAR, ESCRIURE
}
public int valor() { return valor; }
public static void main(String[] args) throws InterruptedException {
final int VOLTES = 1_000_000;
for (int intent = 1; intent <= 3; intent++) {
ComptadorVolatileEncaraTrencat c = new ComptadorVolatileEncaraTrencat();
Thread f1 = new Thread(() -> { for (int i = 0; i < VOLTES; i++) c.incrementar(); });
Thread f2 = new Thread(() -> { for (int i = 0; i < VOLTES; i++) c.incrementar(); });
f1.start(); f2.start();
f1.join(); f2.join();
System.out.printf("Intent %d: esperat %d, real %d, perduts %d%n",
intent, VOLTES * 2, c.valor(), VOLTES * 2 - c.valor());
}
}
}Sortida:
Intent 1: esperat 2000000, real 1223981, perduts 776019
Intent 2: esperat 2000000, real 1341102, perduts 658898
Intent 3: esperat 2000000, real 1189447, perduts 810553Continua perdent gairebé el 40 % dels increments. volatile va resoldre la visibilitat —els dos fils veuen ara el valor més recent a cada lectura— però el forat entre llegir i escriure continua allà, i continua sent la cursa de l'apartat 1.
Els dos exemples junts són la lliçó completa:
| Exemple | Sense volatile |
Amb volatile |
|---|---|---|
| Marcador d'aturada (apartat 3.2) | Es penja: no veu mai el canvi | Funciona: s'atura immediatament |
Comptador valor++ |
Perd increments | Continua perdent increments |
Taula resum de garanties:
| Propietat | volatile |
synchronized |
AtomicInteger (08-06) |
|---|---|---|---|
| Visibilitat | Sí | Sí | Sí |
| No reordenació | Sí | Sí | Sí |
| Atomicitat de lectura/escriptura simple | Sí (incl. long/double) |
Sí | Sí |
| Atomicitat de llegir-modificar-escriure | No | Sí | Sí |
| Exclusió mútua d'un bloc | No | Sí | No |
| Pot provocar interbloqueig | No | Sí | No |
| Cost | Molt baix | Mitjà | Baix |
L'altre error freqüent amb volatile: creure que protegeix l'objecte al qual apunta una referència.
// 'cataleg' volatile garanteix que veus la referencia mes recent...
private volatile List<Material> cataleg = new ArrayList<>();
// ...pero NO protegeix el CONTINGUT de la llista.
cataleg.add(nouLlibre); // continua sent una cursa de dadesvolatile protegeix la variable, no l'objecte. Per al contingut calen panys, una col·lecció concurrent (08-06) o immutabilitat.
synchronized: el monitor intrínsec
synchronized: el monitor intrínsecCada objecte de Java té un monitor associat —també anomenat bloqueig intrínsec o pany—. No el declares: existeix pel fet de ser un objecte. synchronized és la paraula clau que l'adquireix i el deixa anar.
Semàntica exacta:
- En entrar, el fil adquireix el monitor d'
objecte. Si un altre fil el té, quedaBLOCKED(08-03) fins que s'alliberi. - En sortir —pel final del bloc, per un
return, o per una excepció—, el monitor s'allibera. Això últim és important:synchronizedés a prova d'excepcions per construcció, a diferència deReentrantLock. - S'estableixen les relacions happens-before de la regla del monitor: deixar anar hb adquirir després.
synchronized dona dues coses alhora, i aquesta és la seva virtut:
- Exclusió mútua: només un fil alhora executa el bloc.
- Visibilitat: en entrar, el fil veu tot el que va fer el fil anterior que va deixar anar aquell mateix monitor.
La segona s'oblida constantment i és la meitat del valor. Un synchronized no només evita que dos fils trepitgin la dada: garanteix que el segon vegi el que va fer el primer.
El comptador arreglat:
public class ComptadorSincronitzat {
private int valor = 0;
// Metode sincronitzat d'instancia: adquireix el monitor de 'this'.
public synchronized void incrementar() {
valor++; // ara les tres operacions son indivisibles
// respecte a altres fils que facin servir AQUEST monitor
}
// TAMBE sincronitzat. Si no ho estigues, un lector podria veure
// un valor obsolet: l'exclusio mutua sense visibilitat no n'hi ha prou.
public synchronized int valor() {
return valor;
}
public static void main(String[] args) throws InterruptedException {
final int VOLTES = 1_000_000;
for (int intent = 1; intent <= 3; intent++) {
ComptadorSincronitzat c = new ComptadorSincronitzat();
Thread f1 = new Thread(() -> { for (int i = 0; i < VOLTES; i++) c.incrementar(); });
Thread f2 = new Thread(() -> { for (int i = 0; i < VOLTES; i++) c.incrementar(); });
f1.start(); f2.start();
f1.join(); f2.join();
System.out.printf("Intent %d: esperat %d, real %d%n",
intent, VOLTES * 2, c.valor());
}
}
}Sortida:
Intent 1: esperat 2000000, real 2000000
Intent 2: esperat 2000000, real 2000000
Intent 3: esperat 2000000, real 2000000Exacte, sempre, en totes les execucions. El problema obert des de 08-01 està resolt.
El detall que s'oblida: el
gettertambé va sincronitzat. Sivalor()no ho estigués, un lector podria veure un valor obsolet de la seva memòria cau, encara que els escriptors fossin perfectament correctes. Tots els accessos a una dada compartida —lectures incloses— han de fer servir el mateix mecanisme de sincronització. És la regla que més s'incompleix després de l'ifen lloc delwhile.
Reentrada. Els monitors de Java són reentrants: si un fil ja té el monitor, el pot tornar a adquirir sense bloquejar-se. La JVM porta un comptador d'adquisicions i només l'allibera quan arriba a zero.
public class Reentrada {
public synchronized void registrarPrestec(String isbn) {
validar(isbn);
// Crida a UN ALTRE metode sincronitzat del MATEIX objecte.
// Sense reentrada, el fil es bloquejaria a si mateix: interbloqueig
// instantani amb si mateix.
actualitzarIndex(isbn);
}
public synchronized void actualitzarIndex(String isbn) { /* ... */ }
private void validar(String isbn) { /* ... */ }
}Sense reentrada, l'herència seria inviable: un mètode sincronitzat d'una subclasse que cridi super.metodeSincronitzat() es bloquejaria amb si mateix. La reentrada fa que això simplement funcioni.
- Les formes de
synchronized
synchronizedN'hi ha tres, i no són equivalents.
Forma 1: mètode sincronitzat d'instància. Bloqueja this.
public synchronized void afegir(Material m) { /* ... */ }
// Es EXACTAMENT equivalent a:
public void afegir(Material m) {
synchronized (this) { /* ... */ }
}Forma 2: mètode sincronitzat estàtic. Bloqueja l'objecte Class.
public static synchronized void registrarAGlobal(String esdeveniment) { /* ... */ }
// Equivalent a:
public static void registrarAGlobal(String esdeveniment) {
synchronized (Cataleg.class) { /* ... */ }
}Conseqüència crítica que sorprèn molta gent: un mètode d'instància sincronitzat i un mètode estàtic sincronitzat de la mateixa classe fan servir monitors diferents i no s'exclouen entre si. Si tots dos toquen el mateix estat estàtic, hi ha una cursa.
public class DosMonitorsDiferents {
private static int comptadorGlobal = 0;
// Bloqueja 'this'
public synchronized void incrementarMalament() { comptadorGlobal++; }
// Bloqueja 'DosMonitorsDiferents.class'
public static synchronized void incrementarEstatic() { comptadorGlobal++; }
// Aquests dos metodes NO s'exclouen: fan servir panys diferents.
// L'estat estatic que comparteixen NO esta protegit. BUG.
}Forma 3: bloc sincronitzat amb objecte de bloqueig explícit.
private final Object pany = new Object();
public void afegir(Material m) {
// Feina que NO necessita proteccio, fora del bloqueig:
validar(m);
String clau = normalitzar(m.isbn());
synchronized (pany) {
// Seccio critica: el minim imprescindible
materials.add(m);
index.put(clau, m);
}
// Mes feina no critica
registrarAAuditoria(m);
}Aquesta tercera forma és la preferible, per dues raons:
Raó A: granularitat. El mètode sincronitzat bloqueja tot el mètode, inclosa la feina que no ho necessita. Si validar() triga 50 ms, tots els altres fils esperen 50 ms per no res. El bloc protegeix només l'imprescindible.
Raó B: control del pany. synchronized (this) exposa el monitor al món: qualsevol codi extern pot fer synchronized (elMeuCataleg) i bloquejar la teva classe des de fora, deliberadament o per accident. No és teòric: Vector i Hashtable sincronitzen sobre this i aquest és un dels motius pels quals es desaconsellen.
public class Cataleg {
// Pany PRIVAT i FINAL: ningu de fora no el pot adquirir,
// i ningu no el pot reassignar (un pany reassignat deixa de
// protegir: dos fils podrien bloquejar objectes diferents).
private final Object pany = new Object();
// I no facis servir MAI com a pany una cosa aixi:
// private final Integer pany = 42; // <- pot estar internat
// private final String pany = "cat"; // <- literal internat: COMPARTIT
// private final Boolean pany = false; // <- Boolean.FALSE, compartit
// Un literal String o un Integer petit es un objecte UNIC a tota la JVM:
// una altra classe que faci servir el mateix literal comparteix el teu pany sense saber-ho.
}Taula comparativa:
| Forma | Pany | Granularitat | Exposat | Recomanació |
|---|---|---|---|---|
synchronized d'instància |
this |
Tot el mètode | Sí | Només en classes petites i controlades |
synchronized estàtic |
Classe.class |
Tot el mètode | Sí | Només per a estat estàtic |
synchronized (panyPrivat) |
Objecte privat | La que decideixis | No | Preferida |
- Què sincronitzar i què no
Sincronitzar de més és tan dolent com sincronitzar de menys: converteix un programa concurrent en un de seqüencial amb sobrecost afegit.
Regla 1: la secció crítica, tan curta com sigui possible.
// MALAMENT: 300 ms d'E/S dins del bloqueig. Tots els altres fils esperen.
public void registrarPrestec(Prestec p) {
synchronized (pany) {
prestecs.put(p.id(), p);
escriureAlDisc(p); // 300 ms d'E/S
enviarNotificacio(p.empleat()); // 200 ms mes
}
}
// BE: nomes el canvi d'estat en memoria esta protegit.
public void registrarPrestec(Prestec p) {
synchronized (pany) {
prestecs.put(p.id(), p); // microsegons
}
escriureAlDisc(p); // fora del bloqueig
enviarNotificacio(p.empleat()); // fora del bloqueig
}Regla 2: no facis mai E/S dins d'un bloqueig. És un cas particular de la regla 1, però mereix la seva pròpia entrada perquè és l'error de rendiment més freqüent. Una lectura de disc pot trigar 10 ms; una consulta remota, centenars. Mantenir un pany durant aquest temps serialitza tota l'aplicació.
Regla 3: no cridis mai codi aliè amb un pany a la mà. Codi aliè és qualsevol cosa que no controlis: un mètode sobreescriptible, un callback, un oient registrat per l'usuari.
// PERILLOS: 'oients' conte codi que no controles.
synchronized (pany) {
for (OientCataleg o : oients) {
o.materialAfegit(m); // que fa? bloqueja? crida de tornada?
}
}
// SEGUR: copiar sota el pany, notificar fora.
List<OientCataleg> copia;
synchronized (pany) {
copia = new ArrayList<>(oients);
}
for (OientCataleg o : copia) {
o.materialAfegit(m); // sense pany: no ens pot bloquejar
}Un oient que intenti adquirir un altre pany des d'allà pot provocar un interbloqueig; un que cridi de tornada la teva classe, una recursió inesperada. El patró de copiar sota el pany i notificar fora és estàndard, i a 08-06 veuràs que CopyOnWriteArrayList ho fa per tu.
Regla 4: no sincronitzis el que no es comparteix. Sincronitzar una variable local o un objecte confinat a un fil és cost pur. (La JVM de vegades ho detecta i elimina el bloqueig —lock elision—, però no hi comptis.)
Regla 5: documenta la política de sincronització. Un comentari al camp dient què el protegeix val per mitja hora de lectura de codi:
public class RegistrePrestecs {
private final Object pany = new Object();
/** Protegit per 'pany'. */
private final Map<String, Prestec> perId = new HashMap<>();
/** Protegit per 'pany'. Invariant: conte els mateixos prestecs que 'perId'. */
private final Map<Empleat, List<Prestec>> perEmpleat = new HashMap<>();
}Aquest /** Protegit per 'pany'. */ és l'anotació @GuardedBy del llibre Java Concurrency in Practice escrita a mà. Costa una línia i evita que algú —tu, d'aquí a sis mesos— toqui el camp sense el pany.
- Interbloqueig: l'exemple reproduïble
Vas veure un interbloqueig a 08-03 des del punt de vista del diagnòstic. Ara, des del del disseny.
Un interbloqueig necessita quatre condicions simultànies (condicions de Coffman):
- Exclusió mútua: els recursos no es comparteixen.
- Retenir i esperar: un fil en reté un i en demana un altre.
- Sense expropiació: no se li pot treure un pany a un fil.
- Espera circular: existeix un cicle de fils esperant-se.
Amb synchronized, les tres primeres venen donades per la mateixa semàntica del llenguatge. L'única sobre la qual pots actuar és la quarta, i aquesta és tota l'estratègia.
Exemple realista de BiblioTech: transferir un préstec entre dos empleats requereix bloquejar els comptes d'ambdós.
package com.nexussoftware.bibliotech.servei;
import java.util.concurrent.TimeUnit;
public class TransferenciaPrestecs {
/** Cada empleat te el seu propi pany i la seva llista de prestecs. */
static class CompteEmpleat {
final String nom;
final Object pany = new Object();
int prestecs;
CompteEmpleat(String nom, int prestecs) {
this.nom = nom;
this.prestecs = prestecs;
}
}
/**
* VERSIO AMB INTERBLOQUEIG.
* Bloqueja 'origen' i despres 'desti'. Si dos fils transfereixen
* en sentits oposats, es forma el cicle.
*/
static void transferirMalament(CompteEmpleat origen, CompteEmpleat desti, int n) {
synchronized (origen.pany) {
dormir(100); // amplia la finestra de la fallada
synchronized (desti.pany) {
origen.prestecs -= n;
desti.prestecs += n;
System.out.printf(" %s -> %s : %d prestecs%n",
origen.nom, desti.nom, n);
}
}
}
public static void main(String[] args) throws InterruptedException {
CompteEmpleat marta = new CompteEmpleat("Marta Ruiz", 5);
CompteEmpleat diego = new CompteEmpleat("Diego Alonso", 5);
// Fil 1: marta -> diego (bloqueja marta, despres diego)
Thread t1 = new Thread(() -> transferirMalament(marta, diego, 1), "fil-marta-diego");
// Fil 2: diego -> marta (bloqueja diego, despres marta) <-- ORDRE INVERS
Thread t2 = new Thread(() -> transferirMalament(diego, marta, 1), "fil-diego-marta");
t1.start(); t2.start();
t1.join(3000);
t2.join(3000);
if (t1.isAlive() || t2.isAlive()) {
System.out.println("INTERBLOQUEIG: els dos fils continuen vius i bloquejats.");
System.out.println("Estat t1: " + t1.getState());
System.out.println("Estat t2: " + t2.getState());
System.exit(1);
}
}
}Sortida:
Els dos fils en BLOCKED per sempre, sense excepció, sense traça, sense consum de CPU. Diagrama del que ha passat:
sequenceDiagram
participant T1 as fil-marta-diego
participant CM as pany de Marta
participant CD as pany de Diego
participant T2 as fil-diego-marta
T1->>CM: adquireix OK
T2->>CD: adquireix OK
Note over T1,T2: tots dos dormen 100 ms
T1->>CD: demana - el te T2
Note over T1: BLOCKED
T2->>CM: demana - el te T1
Note over T2: BLOCKED
Note over T1,T2: espera circular: cap dels dos el deixa anar.<br/>INTERBLOQUEIG PERMANENT
- Les solucions a l'interbloqueig
Solució A: ordre global d'adquisició
Defineix un ordre total sobre tots els panys i adquireix-los sempre en aquest ordre. Sense espera circular no hi ha interbloqueig; és una garantia matemàtica, no una probabilitat.
L'ordre pot ser qualsevol sempre que sigui total i consistent. Aquí es fa servir System.identityHashCode, que dona un enter estable per objecte:
/**
* VERSIO CORRECTA: ordre global d'adquisicio.
* Els panys es prenen SEMPRE en ordre creixent d'identityHashCode,
* independentment de la direccio de la transferencia.
*/
static void transferirBe(CompteEmpleat origen, CompteEmpleat desti, int n) {
int ho = System.identityHashCode(origen);
int hd = System.identityHashCode(desti);
if (ho < hd) {
synchronized (origen.pany) {
synchronized (desti.pany) {
moure(origen, desti, n);
}
}
} else if (ho > hd) {
synchronized (desti.pany) { // ordre INVERTIT respecte al negoci,
synchronized (origen.pany) { // pero CONSISTENT entre fils
moure(origen, desti, n);
}
}
} else {
// Cas rarissim: collisio d'identityHashCode entre dos objectes
// diferents. Un tercer pany global desempata i garanteix
// que nomes un fil entri en aquest cami alhora.
synchronized (PANY_DESEMPAT) {
synchronized (origen.pany) {
synchronized (desti.pany) {
moure(origen, desti, n);
}
}
}
}
}
private static final Object PANY_DESEMPAT = new Object();
private static void moure(CompteEmpleat origen, CompteEmpleat desti, int n) {
origen.prestecs -= n;
desti.prestecs += n;
}Amb això, els dos fils de l'apartat 10 adquireixen els panys en el mateix ordre, un dels dos guanya, fa la seva feina i el deixa anar. No hi ha interbloqueig possible, ni al teu portàtil ni en producció ni d'aquí a deu anys.
Si els teus objectes tenen un identificador natural —un ISBN, un identificador d'empleat—, fes-lo servir: és més llegible i estable que l'identityHashCode.
// Amb identificador natural, molt mes clar:
CompteEmpleat primer = origen.id().compareTo(desti.id()) < 0 ? origen : desti;
CompteEmpleat segon = (primer == origen) ? desti : origen;
synchronized (primer.pany) {
synchronized (segon.pany) {
moure(origen, desti, n);
}
}Solució B: tryLock amb temps límit
Si no pots imposar un ordre —perquè els panys els trien components que no controles—, l'alternativa és no esperar indefinidament: intentar adquirir amb termini i, si falla, deixar-ho tot anar i reintentar. Requereix ReentrantLock (apartat 13), perquè synchronized no té tryLock.
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TransferenciaAmbTryLock {
static class CompteEmpleat {
final String nom;
final ReentrantLock pany = new ReentrantLock();
int prestecs;
CompteEmpleat(String nom, int prestecs) {
this.nom = nom; this.prestecs = prestecs;
}
}
/**
* Adquireix els dos panys amb termini. Si no aconsegueix tots dos,
* deixa anar el que tingui, espera un temps ALEATORI i reintenta.
* L'atzar es essencial: sense ell, dos fils podrien reintentar
* sincronitzats eternament (livelock, apartat 12).
*/
static boolean transferir(CompteEmpleat origen, CompteEmpleat desti,
int n, int maxIntents) throws InterruptedException {
for (int intent = 0; intent < maxIntents; intent++) {
boolean tincOrigen = false;
boolean tincDesti = false;
try {
tincOrigen = origen.pany.tryLock(50, TimeUnit.MILLISECONDS);
if (tincOrigen) {
tincDesti = desti.pany.tryLock(50, TimeUnit.MILLISECONDS);
}
if (tincOrigen && tincDesti) {
origen.prestecs -= n;
desti.prestecs += n;
return true; // exit
}
} finally {
// SEMPRE deixar anar el que s'ha adquirit, en ordre invers.
if (tincDesti) desti.pany.unlock();
if (tincOrigen) origen.pany.unlock();
}
// Retroces aleatori abans de reintentar.
TimeUnit.MILLISECONDS.sleep(
ThreadLocalRandom.current().nextInt(10, 60));
}
return false; // intents exhaurits
}
}Comparació de les dues solucions:
| Ordre global | tryLock amb termini |
|
|---|---|---|
| Garantia | Absoluta: l'interbloqueig és impossible | Probabilística: s'evita, no es prohibeix |
| Cost | Cap en temps d'execució | Reintents i esperes |
| Requisit | Poder ordenar els panys | Poder abandonar i reintentar l'operació |
| Complexitat del codi | Baixa | Mitjana-alta |
| Quan fer-la servir | Sempre que sigui possible | Quan l'ordre no es pot imposar |
La recomanació és clara: ordre global sempre que puguis. tryLock és el pla B.
Solució C: la millor de totes, un sol pany
Abans de complicar-se: de veritat calen dos panys? Moltes vegades un únic pany més gruixut resol el problema amb un cost de contenció perfectament assumible. Un pany no es pot interbloquejar amb si mateix.
// Si les transferencies no son el 90% de la carrega, aixo es
// mes simple, mes segur i probablement igual de rapid.
private static final Object PANY_TRANSFERENCIES = new Object();
static void transferir(CompteEmpleat origen, CompteEmpleat desti, int n) {
synchronized (PANY_TRANSFERENCIES) {
origen.prestecs -= n;
desti.prestecs += n;
}
}Mesura abans d'optimitzar. La granularitat fina és una optimització, i com tota optimització, té un cost en complexitat i en risc.
- Inanició i livelock
Inanició (starvation). Un fil no aconsegueix mai el recurs que necessita perquè uns altres se l'emporten sempre. Causes habituals:
- Prioritats molt dispars (08-02: no les facis servir).
- Un pany no equitatiu que afavoreix sistemàticament el fil que acaba de deixar-lo anar, perquè la seva memòria cau continua calenta.
- Un fil que reté un pany massa temps (E/S dins del bloqueig, apartat 9).
Solució: seccions crítiques curtes i, si de veritat cal, un pany equitatiu:
// El parametre true activa la politica FIFO: el pany es concedeix
// al fil que porta mes temps esperant.
// COST: molt menys rendiment (sovint 10x o mes), perque impedeix
// la reentrada oportunista i forca canvis de context.
// Fes-lo servir nomes si has mesurat inanicio real.
ReentrantLock equitatiu = new ReentrantLock(true);Livelock. Els fils sí que estan actius i executant, però cap no progressa: reaccionen contínuament els uns als altres. És el cas de dues persones que es creuen en un passadís i s'aparten totes dues cap al mateix costat, una vegada i una altra.
En codi, el tryLock sense retrocés aleatori és l'exemple clàssic:
// LIVELOCK: si els dos fils entren en fase, poden reintentar
// eternament, cedint sempre alhora.
while (true) {
if (a.tryLock()) {
if (b.tryLock()) { ferFeina(); return; }
a.unlock(); // cedeix
}
// sense espera aleatoria: els dos fils ho tornen a intentar
// exactament alhora, una altra vegada, i una altra
}La solució és el retrocés aleatori (randomized backoff), com a l'exemple de l'apartat 11: dormir un temps aleatori abans de reintentar trenca la sincronia. És la mateixa idea que fa servir Ethernet per a les col·lisions.
Diferència amb l'interbloqueig, que importa en diagnosticar: en un interbloqueig els fils estan en BLOCKED i consumeixen 0 % de CPU; en un livelock estan en RUNNABLE i consumeixen CPU al màxim. Si la teva aplicació no avança però la CPU està al 100 %, sospita livelock, no interbloqueig — i jstack no t'ho dirà amb un missatge Found one Java-level deadlock.
ReentrantLock davant de synchronized
ReentrantLock davant de synchronizedjava.util.concurrent.locks.ReentrantLock fa el mateix que synchronized i hi afegeix coses que synchronized no pot fer. A canvi, s'ha de deixar anar a mà.
El patró obligatori, sense excepcions:
import java.util.concurrent.locks.ReentrantLock;
public class ComptadorAmbLock {
private final ReentrantLock pany = new ReentrantLock();
private int valor = 0;
public void incrementar() {
pany.lock();
try {
valor++;
} finally {
// OBLIGATORI al finally: si el cos llanca una excepcio
// i l'unlock es fora, el pany NO s'allibera MAI
// i tota l'aplicacio es bloqueja. Es l'error numero u
// de ReentrantLock, i el motiu pel qual synchronized
// continua sent preferible quan no necessites res mes.
pany.unlock();
}
}
}
lock()va FORA deltry. Si estigués a dins i fallés l'adquisició, elfinallyfariaunlock()d'un pany que no es té:IllegalMonitorStateException. L'ordre correcte éslock(); try { ... } finally { unlock(); }.
El que ReentrantLock afegeix:
1. tryLock(): adquirir sense esperar.
if (pany.tryLock()) { // no bloqueja mai
try { operacio(); } finally { pany.unlock(); }
} else {
// pla B: encuar, avisar l'usuari, reintentar mes tard
}2. tryLock(temps, unitat): adquirir amb termini. La base de la solució B a l'interbloqueig.
3. lockInterruptibly(): espera cancel·lable.
try {
pany.lockInterruptibly(); // respon a interrupt() mentre espera
try { operacio(); } finally { pany.unlock(); }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}Això és impossible amb synchronized: un fil BLOCKED esperant un monitor no respon a interrupt(). Si l'operació pot trigar i vols poder cancel·lar-la, ReentrantLock és l'única opció.
4. Equitat opcional: new ReentrantLock(true).
5. Diverses Condition per pany (apartat 14).
6. Introspecció: isLocked(), getHoldCount(), getQueueLength() — útils per a diagnòstic i mètriques.
Taula de decisió:
| Característica | synchronized |
ReentrantLock |
|---|---|---|
| Alliberament automàtic | Sí, fins i tot amb excepció | No: finally obligatori |
| Sintaxi | Paraula clau, impossible oblidar-se'n | Codi explícit |
| Intentar sense bloquejar | No | tryLock() |
| Termini d'espera | No | tryLock(t, u) |
| Espera interrompible | No | lockInterruptibly() |
| Equitat | No | Opcional |
| Condicions d'espera | Una (wait/notify) |
Diverses (newCondition()) |
| Visible als bolcats | Sí, - locked <0x...> |
Sí (amb jcmd Thread.print -l) |
| Rendiment | Igual des de Java 6 | Igual |
| Llegibilitat | Major | Menor |
La recomanació: fes servir synchronized per defecte. És més curt, impossible d'oblidar-se d'alliberar i perfectament ràpid des que Java 6 va introduir el biaix i l'eixamplament de bloquejos. Passa a ReentrantLock només quan necessitis tryLock, termini, interrompibilitat, equitat o diverses condicions.
Condition: substitut de wait/notify
Condition: substitut de wait/notifyUn ReentrantLock pot crear diversos objectes Condition, cadascun amb la seva pròpia cua d'espera. És la solució elegant al problema de notify davant de notifyAll de 08-03: pots senyalitzar exactament el grup correcte.
Object (amb synchronized) |
Condition (amb Lock) |
|---|---|
wait() |
await() |
wait(ms) |
await(t, unitat) |
notify() |
signal() |
notifyAll() |
signalAll() |
| Una sola cua per objecte | Tantes com vulguis |
| — | awaitUninterruptibly() |
Cua acotada amb dues condicions separades:
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Cua acotada de reserves amb dues condicions separades.
*
* Avantatge sobre wait/notifyAll (08-03): en alliberar un forat nomes es
* desperta un PRODUCTOR, i en afegir un element nomes un CONSUMIDOR.
* Amb notifyAll es despertaven tots i la majoria es tornava a adormir.
*
* NOTA: en produccio faries servir ArrayBlockingQueue (08-06), que es
* exactament aixo ja implementat i provat.
*/
public class CuaReservesAmbCondition {
private final ReentrantLock pany = new ReentrantLock();
// DUES condicions sobre el MATEIX pany.
private final Condition hiHaForat = pany.newCondition();
private final Condition hiHaElement = pany.newCondition();
private final Deque<String> cua = new ArrayDeque<>();
private final int capacitat;
public CuaReservesAmbCondition(int capacitat) {
this.capacitat = capacitat;
}
public void posar(String reserva) throws InterruptedException {
pany.lock();
try {
// WHILE, igual que amb wait(): les mateixes tres raons de 08-03.
while (cua.size() == capacitat) {
hiHaForat.await(); // deixa anar el pany i espera
}
cua.addLast(reserva);
hiHaElement.signal(); // desperta UN consumidor.
// Segur: en aquesta cua nomes esperen
// consumidors, i tots son
// intercanviables.
} finally {
pany.unlock();
}
}
public String prendre() throws InterruptedException {
pany.lock();
try {
while (cua.isEmpty()) {
hiHaElement.await();
}
String r = cua.pollFirst();
hiHaForat.signal(); // desperta UN productor
return r;
} finally {
pany.unlock();
}
}
public int mida() {
pany.lock();
try {
return cua.size();
} finally {
pany.unlock();
}
}
}Amb Condition separades, signal() sí que és segur, perquè tots els fils d'aquella cua esperen exactament la mateixa condició i són intercanviables. És la diferència entre despertar els deu fils que esperen —dels quals nou es tornen a adormir— i despertar l'únic que pot avançar.
ReadWriteLock: llegir molt, escriure poc
ReadWriteLock: llegir molt, escriure pocUn pany exclusiu tracta igual lectors i escriptors. Però dos lectors no es destorben: llegir no modifica res. Si la teva estructura es llegeix cent vegades per cada escriptura —exactament el cas del Cataleg de BiblioTech—, un pany exclusiu malbarata gairebé tot el paral·lelisme disponible.
ReentrantReadWriteLock ofereix dos panys coordinats:
- Pany de lectura: compartit. Molts fils alhora.
- Pany d'escriptura: exclusiu. Exclou lectors i altres escriptors.
| Situació | Es permet? |
|---|---|
| Lector + lector | Sí, simultanis |
| Lector + escriptor | No |
| Escriptor + escriptor | No |
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Material;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Cataleg segur per a diversos fils, optimitzat per a lectura.
*
* Justificacio del ReadWriteLock: a BiblioTech es consulta el cataleg
* desenes de vegades per cada alta (cercar per ISBN, llistar, filtrar per tipus),
* i les altes arriben en lots durant la importacio.
*/
public class CatalegSegur {
private final ReentrantReadWriteLock pany = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock lectura = pany.readLock();
private final ReentrantReadWriteLock.WriteLock escriptura = pany.writeLock();
/** Protegit per 'pany'. */
private final List<Material> materials = new ArrayList<>();
/** Protegit per 'pany'. Invariant: una entrada per material. */
private final Map<String, Material> index = new HashMap<>();
/** Protegit per 'pany'. Invariant: mateixos ISBN que 'index'. */
private final Set<String> isbnRegistrats = new HashSet<>();
// ---------- ESCRIPTURES: pany exclusiu ----------
public boolean afegir(Material m) {
escriptura.lock();
try {
if (!isbnRegistrats.add(m.isbn())) {
return false; // ja existia
}
materials.add(m);
index.put(m.isbn(), m);
return true;
} finally {
escriptura.unlock();
}
}
public boolean eliminar(String isbn) {
escriptura.lock();
try {
Material m = index.remove(isbn);
if (m == null) return false;
materials.remove(m);
isbnRegistrats.remove(isbn);
return true;
} finally {
escriptura.unlock();
}
}
/** Substitucio atomica completa: el que fa servir la importacio de 08-02. */
public void substituirTot(List<Material> nous) {
escriptura.lock();
try {
materials.clear();
index.clear();
isbnRegistrats.clear();
for (Material m : nous) {
if (isbnRegistrats.add(m.isbn())) {
materials.add(m);
index.put(m.isbn(), m);
}
}
} finally {
escriptura.unlock();
}
}
// ---------- LECTURES: pany compartit ----------
public Material cercarPerIsbn(String isbn) {
lectura.lock();
try {
return index.get(isbn);
} finally {
lectura.unlock();
}
}
public int mida() {
lectura.lock();
try {
return materials.size();
} finally {
lectura.unlock();
}
}
/**
* Retorna una COPIA. Retornar la llista interna seria una fallada de
* seguretat de fils: qui la rebi podria iterar-la sense pany
* mentre un altre fil la modifica -> ConcurrentModificationException
* (05-02) o alguna cosa pitjor.
*/
public List<Material> llistar() {
lectura.lock();
try {
return new ArrayList<>(materials); // copia sota el pany
} finally {
lectura.unlock();
}
}
}Quan compensa un ReadWriteLock:
| Condició | Per què importa |
|---|---|
| Lectures ≫ escriptures (≥ 5:1, idealment 20:1) | Amb poques lectures, el cost més gran del pany no s'amortitza |
| Les lectures duren alguna cosa | Si duren nanosegons, la sobrecàrrega domina |
| Hi ha contenció real | Sense diversos fils concurrents no s'hi guanya res |
Quan NO compensa: operacions molt curtes, proporció equilibrada de lectures i escriptures, o poca concurrència. En aquests casos un synchronized senzill sol ser més ràpid, perquè ReentrantReadWriteLock manté més estat intern.
Degradació i promoció. Es pot degradar —tenir el pany d'escriptura i adquirir el de lectura abans de deixar-lo anar— però no promocionar: intentar adquirir el d'escriptura tenint el de lectura produeix un interbloqueig instantani amb si mateix.
// PROHIBIT: promocio. Es penja per sempre.
lectura.lock();
escriptura.lock(); // espera que es deixin anar TOTES les lectures,
// inclosa la NOSTRA. Interbloqueig.
// PERMES: degradacio (escriptura -> lectura).
escriptura.lock();
try {
modificar();
lectura.lock(); // adquirir lectura ABANS de deixar anar escriptura
} finally {
escriptura.unlock(); // ara nomes tenim lectura
}
try {
llegirElQueAcabemDEscriure();
} finally {
lectura.unlock();
}Alternativa moderna:
StampedLock(Java 8). Afegeix lectura optimista: llegeixes sense bloquejar res i després valides si hi va haver escriptures entremig; si n'hi va haver, reintentes amb lectura real. És més ràpid sota molta càrrega de lectura, però no és reentrant i la seva API és fàcil de fer servir malament. Menciona'l, mesura'l si tens un coll d'ampolla demostrat, i no el facis servir per defecte.
- Publicació segura i immutabilitat
Publicar un objecte és fer-lo visible a altres fils. Fer-ho malament produeix una fallada desconcertant: un altre fil pot veure un objecte a mig construir.
// PUBLICACIO INSEGURA
public class Registre {
public static Cataleg instancia; // sense volatile, sense final
public static void inicialitzar() {
instancia = new Cataleg(); // (1) reservar (2) construir (3) assignar
// Les fases (2) i (3) es poden REORDENAR: un altre fil pot veure
// 'instancia' no nulla apuntant a un objecte sense construir.
}
}Formes segures de publicar:
- Inicialitzar-lo des d'un inicialitzador estàtic (la JVM sincronitza la càrrega de classes).
- Desar-lo en un camp
volatileo en unaAtomicReference(08-06). - Desar-lo en un camp
finald'un objecte correctament construït. - Desar-lo en un camp protegit per un pany, i llegir-lo amb el mateix pany.
- Posar-lo en una col·lecció concurrent (08-06).
La garantia dels camps final mereix un paràgraf propi, perquè és la que fa que la immutabilitat funcioni:
Si un objecte només té camps
finali no deixa escaparthisdurant la construcció, qualsevol fil que n'obtingui una referència veurà els seus camps correctament inicialitzats, sense necessitat de cap sincronització.
Un objecte immutable és aquell que:
- Té tots els seus camps
final. - No exposa cap mètode que canviï el seu estat.
- No deixa escapar
thisal constructor. - Si conté referències a objectes mutables, fa còpies defensives en entrar i en sortir.
Un objecte immutable és segur per a qualsevol nombre de fils, sense cap pany, per sempre. És l'estratègia més potent de tota la concurrència.
I aquí ve el bo: els record de 04-07 són immutables per construcció.
package com.nexussoftware.bibliotech.domini;
/**
* Fitxa immutable: tots els seus camps son final pel fet de ser un record.
* Compartible entre qualsevol nombre de fils sense sincronitzacio.
*/
public record Fitxa(String isbn, String titol, String autor, int exemplars) { }
/**
* ResumSessio immutable. Compte: si un record conte una colleccio,
* la colleccio ha de ser tambe immutable, o el record nomes es
* "superficialment" immutable.
*/
public record ResumSessio(long iniciMs, long fiMs,
int prestecs, int devolucions,
List<String> incidencies) {
/** Constructor compacte amb copia defensiva: fa la llista immutable. */
public ResumSessio {
incidencies = List.copyOf(incidencies); // copia IMMUTABLE
}
}El truc del constructor compacte és important: sense List.copyOf, qui va construir el record podria continuar modificant la llista original, i el record deixaria de ser immutable de veritat.
Com es canvia l'estat d'un objecte immutable: no es canvia. Se'n crea un de nou i es substitueix la referència de forma atòmica:
public class EstadistiquesBiblioTech {
/** Instantania immutable de les estadistiques. */
public record Instantania(long prestecs, long devolucions, long multes) {
Instantania ambPrestec() { return new Instantania(prestecs + 1, devolucions, multes); }
Instantania ambDevolucio() { return new Instantania(prestecs, devolucions + 1, multes); }
}
// La REFERENCIA es volatile: els lectors sempre veuen una
// instantania completa i coherent, mai una a mitges.
private volatile Instantania actual = new Instantania(0, 0, 0);
/** Lectura sense cap bloqueig: coherent per immutabilitat. */
public Instantania instantania() {
return actual;
}
/**
* L'escriptura SI que necessita sincronitzacio: 'actual = actual.ambPrestec()'
* es llegir-modificar-escriure, i volatile no ho fa atomic (apartat 6).
* A 08-06 aixo es resoldra amb AtomicReference.updateAndGet, sense pany.
*/
public synchronized void registrarPrestec() {
actual = actual.ambPrestec();
}
}Aquest patró —estat immutable + referència volatile— dona lectures sense cap bloqueig i sempre coherents. És enormement valuós quan es llegeix molt més del que s'escriu.
- Confinament: la millor sincronització és no compartir
Totes les tècniques anteriors gestionen la compartició. La millor estratègia és eliminar-la.
Confinament a la pila. Una variable local viu a la pila del fil: és privada per construcció (08-01). Si construeixes un objecte dins d'un mètode, el fas servir i no el deixes escapar, no hi ha res a sincronitzar.
public Informe processarLot(List<String> linies) {
// TOT local: cada fil te la seva propia llista i el seu propi comptador.
// Cap pany, cap cursa possible.
List<Material> acceptats = new ArrayList<>();
int descartats = 0;
for (String l : linies) {
try {
acceptats.add(lector.aMaterial(l));
} catch (FormatInvalidException e) {
descartats++;
}
}
// L'unic punt de contacte amb estat compartit, i esta protegit:
cataleg.afegirTots(acceptats);
return new Informe(acceptats.size(), descartats);
}Confinament en un fil amb ThreadLocal. Cada fil té la seva pròpia còpia de la variable:
import java.text.NumberFormat;
import java.util.Locale;
public class FormatMultes {
// NumberFormat NO es segur per a diversos fils: compartir una instancia
// produeix resultats corruptes sota carrega (un classic molt dificil de
// diagnosticar perque falla poc i de forma silenciosa).
// ThreadLocal dona una instancia per fil: segur i sense panys.
private static final ThreadLocal<NumberFormat> FORMAT =
ThreadLocal.withInitial(() -> NumberFormat.getCurrencyInstance(
Locale.forLanguageTag("ca-ES")));
public static String formatar(double quantitat) {
return FORMAT.get().format(quantitat);
}
/**
* IMPORTANT: en un pool de fils (08-05) els fils es reutilitzen
* indefinidament, aixi que un ThreadLocal no s'allibera mai sol.
* Si desa alguna cosa gran o sensible, cal netejar-lo:
*/
public static void netejar() {
FORMAT.remove();
}
}Avís sobre
ThreadLocali pools. És una font coneguda de fuites de memòria i de contaminació de dades entre peticions: un fil de pool que va atendre Marta Ruiz i no va netejar el seuThreadLocalpot exposar aquesta dada a la petició de Diego Alonso. Regla: si fas servirThreadLocalen un pool, neteja'l en unfinally.
Confinament per disseny: divideix, processa a part i combina al final.
// Cada fil processa EL SEU tros i produeix EL SEU resultat parcial.
// No hi ha estat compartit durant el calcul, nomes en combinar.
// Es el model de ForkJoinPool (08-05) i dels streams parallels (10-04).
List<Informe> parcials = new ArrayList<>();
// ... cada fil retorna el seu Informe, i al final:
Informe total = combinar(parcials); // un sol fil, sense panysLa jerarquia d'estratègies, de millor a pitjor:
| Estratègia | Cost de sincronització | Complexitat | Quan |
|---|---|---|---|
| 1. No compartir (confinament) | Cap | Mínima | Sempre que es pugui |
| 2. Compartir immutable | Cap | Baixa | Dades que no canvien |
3. Compartir amb volatile |
Molt baix | Baixa | Marcadors, un sol escriptor |
| 4. Compartir amb atòmics (08-06) | Baix | Baixa | Comptadors, referències |
| 5. Compartir amb col·lecció concurrent (08-06) | Baix-mitjà | Baixa | Mapes, llistes, cues |
| 6. Compartir amb pany | Mitjà | Mitjana | Invariants entre diversos camps |
| 7. Compartir sense res | — | — | Bug |
Comença sempre per dalt i baixa només quan calgui. La majoria del codi concurrent que s'escriu a la indústria està al nivell 6 quan podria estar a l'1 o al 2.
- BiblioTech: un catàleg segur per a diversos fils
Aplicació completa. RegistrePrestecs té dos mapes que s'han de mantenir coherents entre si, cosa que obliga a un pany —un invariant que abasta dues estructures no es pot mantenir amb atòmics ni amb col·leccions concurrents independents—.
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Empleat;
import com.nexussoftware.bibliotech.domini.Prestec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Registre de prestecs segur per a diversos fils.
*
* POLITICA DE SINCRONITZACIO
* --------------------------
* Tot l'estat mutable esta protegit per 'pany'.
* Les consultes fan servir el pany de lectura (compartit);
* les modificacions, el d'escriptura (exclusiu).
*
* INVARIANT: 'perId' i 'perEmpleat' contenen exactament el mateix
* conjunt de prestecs. Aquest invariant abasta DUES estructures, i per
* aixo no n'hi ha prou amb fer servir dos ConcurrentHashMap (08-06): caldria que
* les dues actualitzacions fossin una sola operacio atomica, i aixo nomes
* ho dona un pany.
*/
public class RegistrePrestecsSegur {
private final ReentrantReadWriteLock pany = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock lectura = pany.readLock();
private final ReentrantReadWriteLock.WriteLock escriptura = pany.writeLock();
/** Protegit per 'pany'. */
private final Map<String, Prestec> perId = new HashMap<>();
/** Protegit per 'pany'. Invariant: coherent amb 'perId'. */
private final Map<Empleat, List<Prestec>> perEmpleat = new HashMap<>();
// ---------- ESCRIPTURA ----------
/**
* Registra un prestec. Les dues actualitzacions ocorren sota el mateix
* pany, aixi que cap lector no pot veure l'estat intermedi en el
* qual el prestec es a 'perId' pero encara no a 'perEmpleat'.
*/
public void registrar(Prestec p) {
escriptura.lock();
try {
if (perId.putIfAbsent(p.id(), p) != null) {
throw new IllegalStateException("Prestec duplicat: " + p.id());
}
perEmpleat.computeIfAbsent(p.empleat(), e -> new ArrayList<>()).add(p);
} finally {
escriptura.unlock();
}
}
public Prestec retornar(String idPrestec) {
escriptura.lock();
try {
Prestec p = perId.remove(idPrestec);
if (p == null) {
throw new PrestecNoTrobatException(idPrestec);
}
List<Prestec> dEmpleat = perEmpleat.get(p.empleat());
if (dEmpleat != null) {
dEmpleat.remove(p);
if (dEmpleat.isEmpty()) {
perEmpleat.remove(p.empleat()); // no deixar llistes buides
}
}
return p;
} finally {
escriptura.unlock();
}
}
// ---------- LECTURA ----------
public Prestec cercar(String idPrestec) {
lectura.lock();
try {
return perId.get(idPrestec);
} finally {
lectura.unlock();
}
}
/** Retorna una COPIA: la llista interna no surt mai del pany. */
public List<Prestec> prestecsDe(Empleat e) {
lectura.lock();
try {
List<Prestec> l = perEmpleat.get(e);
return l == null ? List.of() : new ArrayList<>(l);
} finally {
lectura.unlock();
}
}
public int actius() {
lectura.lock();
try {
return perId.size();
} finally {
lectura.unlock();
}
}
/**
* OPERACIO COMPOSTA ATOMICA.
*
* Aquest metode existeix precisament perque fer-ho des de fora seria
* un comprovar-despres-actuar insegur:
*
* if (registre.actius() < MAX) registre.registrar(p); // BUG
*
* Entre la comprovacio i l'accio, un altre fil pot registrar i
* superar el limit. Encapsular l'operacio completa dins del
* pany es l'unica forma correcta.
*/
public boolean registrarSiHiHaLloc(Prestec p, int maximPerEmpleat) {
escriptura.lock();
try {
List<Prestec> actuals = perEmpleat.get(p.empleat());
if (actuals != null && actuals.size() >= maximPerEmpleat) {
return false;
}
perId.put(p.id(), p);
perEmpleat.computeIfAbsent(p.empleat(), e -> new ArrayList<>()).add(p);
return true;
} finally {
escriptura.unlock();
}
}
}I la prova d'esforç que demostra que funciona:
package com.nexussoftware.bibliotech.servei;
import java.util.concurrent.TimeUnit;
public class ProvaConcurrenciaRegistre {
public static void main(String[] args) throws InterruptedException {
final int FILS = 8;
final int PER_FIL = 5_000;
RegistrePrestecsSegur registre = new RegistrePrestecsSegur();
Empleat marta = new Empleat("E-001", "Marta Ruiz");
Thread[] fils = new Thread[FILS];
long inici = System.nanoTime();
for (int f = 0; f < FILS; f++) {
final int idFil = f;
fils[f] = new Thread(() -> {
for (int i = 0; i < PER_FIL; i++) {
registre.registrar(new Prestec(
"P-" + idFil + "-" + i, marta, "978-0000000001"));
}
}, "bibliotech-registrador-" + f);
fils[f].start();
}
for (Thread f : fils) f.join();
long ms = (System.nanoTime() - inici) / 1_000_000;
int esperat = FILS * PER_FIL;
int real = registre.actius();
int aLlista = registre.prestecsDe(marta).size();
System.out.println("Esperats : " + esperat);
System.out.println("A perId : " + real);
System.out.println("A perEmpleat : " + aLlista);
System.out.println("Invariant coherent : " + (real == aLlista));
System.out.println("Temps : " + ms + " ms");
System.out.println(real == esperat && real == aLlista
? "CORRECTE"
: "FALLADA: hi ha una cursa de dades");
}
}Sortida:
Esperats : 40000
A perId : 40000
A perEmpleat : 40000
Invariant coherent : true
Temps : 187 ms
CORRECTEExecuta aquesta prova deu vegades. Ha de donar CORRECTE les deu. Després treu els lock()/unlock() de registrar i torna a executar-la: veuràs recomptes diferents a cada execució, els dos mapes descompassats, i amb sort alguna ConcurrentModificationException o fins i tot un HashMap corrupte amb un bucle infinit a get() —un fenomen real que s'explica a 08-06—.
Errors Comuns i Consells
Error 1: creure que volatile fa atòmic un increment. L'error conceptual número u del tema. volatile dona visibilitat, no atomicitat. Per a comptadors: synchronized o AtomicInteger (08-06).
Error 2: sincronitzar els escriptors però no els lectors. L'exclusió mútua sense visibilitat no serveix: un lector sense sincronitzar pot veure un valor obsolet indefinidament. Tots els accessos, lectures incloses, han de fer servir el mateix mecanisme.
Error 3: fer servir panys diferents per a la mateixa dada. Un mètode d'instància sincronitzat i un altre d'estàtic sincronitzat fan servir monitors diferents i no s'exclouen. Si toquen el mateix estat, hi ha una cursa.
Error 4: sincronitzar sobre un objecte reassignable o compartit sense voler. synchronized (this) exposa el pany; synchronized sobre un String literal, un Integer petit o un Boolean fa servir un objecte internat que una altra classe pot compartir sense saber-ho. Fes servir private final Object pany = new Object().
Error 5: unlock() fora del finally. Si el cos llança, el pany no s'allibera mai i l'aplicació es bloqueja. És el motiu principal per preferir synchronized.
Error 6: lock() dins del try. Si falla l'adquisició, el finally intenta deixar anar un pany que no es té: IllegalMonitorStateException que emmascara l'error real.
Error 7: fer E/S dins d'un bloqueig. Serialitza l'aplicació sencera. Prepara les dades a dins, escriu a fora.
Error 8: cridar codi aliè amb el pany a la mà. Oients, callbacks, mètodes sobreescriptibles: poden bloquejar, cridar de tornada o interbloquejar. Copia sota el pany, notifica fora.
Error 9: adquirir dos panys en ordre diferent segons el camí. La causa del 90 % dels interbloquejos. Imposa un ordre global i documenta'l.
Error 10: promocionar de lectura a escriptura en un ReadWriteLock. Interbloqueig instantani amb si mateix. La degradació sí que està permesa; la promoció no.
Error 11: retornar la col·lecció interna des d'un mètode sincronitzat. El pany protegeix l'accés, però qui rep la referència la pot iterar sense pany. Retorna una còpia o una vista immutable.
Error 12: fer servir synchronized quan n'hi havia prou amb no compartir. Abans de posar un pany, comprova si l'objecte pot ser local, immutable o confinat a un fil.
Consell 1: documenta cada camp compartit amb /** Protegit per 'X'. */. Una línia que evita hores d'arqueologia.
Consell 2: encapsula les operacions compostes dins de la classe. registrarSiHiHaLloc existeix perquè if (actius() < MAX) registrar(p) des de fora és un comprovar-després-actuar insegur. Si la teva API obliga el client a compondre dues crides, la teva API té un bug.
Consell 3: prova amb pressió real. Vuit fils, desenes de milers d'operacions, i repetit deu vegades. Un test de dos fils i deu operacions no detecta res.
Consell 4: prefereix synchronized per defecte. Passa a ReentrantLock només quan necessitis tryLock, termini, interrompibilitat, equitat o diverses Condition.
Consell 5: mesura abans d'afinar la granularitat. Un pany gruixut és més simple i més segur. Divideix-lo només quan hagis demostrat que és el coll d'ampolla.
Consell 6: quan dubtis, fes-ho immutable. Un record amb camps final no necessita sincronització, no es pot corrompre i no pot interbloquejar.
Exercicis
Exercici 1: Del comptador trencat al comptador correcte
Escriu ComparativaComptadors amb quatre implementacions d'un comptador que suporti incrementar() i valor():
ComptadorInsegur:intsimple.ComptadorVolatile:volatile int.ComptadorSincronitzat:synchronized.ComptadorAmbLock:ReentrantLock.
Sotmet cadascuna a 8 fils × 500.000 increments, comprova si el resultat és correcte i mesura el temps amb System.nanoTime(). Imprimeix una taula amb implementació, resultat, error i increments per mil·lisegon. Comenta per què les dues primeres fallen i per què les dues últimes tenen temps semblants.
Exercici 2: Interbloqueig i la seva correcció
Modela dues sales de reunions de BiblioTech (SalaReunions amb el seu propi pany). Escriu ReservaDobleSala amb:
- Un mètode
reservarMalament(SalaReunions a, SalaReunions b)que adquireixi els dos panys en l'ordre en què se li passen, amb unsleep(100)entre tots dos, i unmainque el interbloquegi de forma reproduïble amb dos fils que reservin en sentits oposats, detectant-ho ambjoin(3000)+isAlive(). - Un mètode
reservarBe(...)que apliqui l'ordre global d'adquisició fent servir l'identificador de sala, i demostri que ja no s'interbloqueja ni en 1.000 intents amb 8 fils. - Un mètode
reservarAmbTryLock(...)ambReentrantLock.tryLock(50, MILLISECONDS), retrocés aleatori i un màxim d'intents, que informi de quantes vegades va haver de reintentar.
Exercici 3: Memòria cau de fitxes amb ReadWriteLock i mesura
Reimplementa CacheFitxes de BiblioTech com a memòria cau segura per a diversos fils amb ReentrantReadWriteLock, amb Fitxa obtenir(String isbn) (que consulta la memòria cau i, si falla, la calcula amb un sleep(20) simulat i la desa), void invalidar(String isbn) i int mida(). Afegeix comptadors d'encerts i fallades protegits pel mateix pany.
Després escriu una prova amb 10 fils que facin 2.000 consultes cadascun sobre un conjunt de 50 ISBN, mesuri el temps total i el percentatge d'encerts, i compari el resultat amb una versió que faci servir un únic synchronized a tots els mètodes. Explica el resultat obtingut.
Solucions
Solució a l'Exercici 1
import java.util.concurrent.locks.ReentrantLock;
public class ComparativaComptadors {
interface Comptador {
void incrementar();
long valor();
String nom();
}
/** 1. INSEGUR: valor++ es llegir-modificar-escriure sense proteccio. */
static class ComptadorInsegur implements Comptador {
private long valor = 0;
public void incrementar() { valor++; }
public long valor() { return valor; }
public String nom() { return "int simple"; }
}
/** 2. VOLATILE: resol la visibilitat, NO l'atomicitat. */
static class ComptadorVolatile implements Comptador {
private volatile long valor = 0;
public void incrementar() { valor++; }
public long valor() { return valor; }
public String nom() { return "volatile"; }
}
/** 3. SYNCHRONIZED: exclusio mutua + visibilitat. Correcte. */
static class ComptadorSincronitzat implements Comptador {
private long valor = 0;
public synchronized void incrementar() { valor++; }
// El getter TAMBE sincronitzat: sense ell, un lector podria
// veure un valor obsolet de la seva memoria cau.
public synchronized long valor() { return valor; }
public String nom() { return "synchronized"; }
}
/** 4. REENTRANTLOCK: equivalent, amb unlock al finally. */
static class ComptadorAmbLock implements Comptador {
private final ReentrantLock pany = new ReentrantLock();
private long valor = 0;
public void incrementar() {
pany.lock(); // FORA del try
try { valor++; }
finally { pany.unlock(); } // SEMPRE al finally
}
public long valor() {
pany.lock();
try { return valor; }
finally { pany.unlock(); }
}
public String nom() { return "ReentrantLock"; }
}
static long mesurar(Comptador c, int fils, int perFil) throws InterruptedException {
Thread[] ts = new Thread[fils];
long inici = System.nanoTime();
for (int i = 0; i < fils; i++) {
ts[i] = new Thread(() -> {
for (int j = 0; j < perFil; j++) c.incrementar();
}, "sumador-" + i);
ts[i].start();
}
for (Thread t : ts) t.join();
return System.nanoTime() - inici;
}
public static void main(String[] args) throws InterruptedException {
final int FILS = 8;
final int PER_FIL = 500_000;
final long ESPERAT = (long) FILS * PER_FIL;
Comptador[] comptadors = {
new ComptadorInsegur(), new ComptadorVolatile(),
new ComptadorSincronitzat(), new ComptadorAmbLock()
};
// Escalfament: sense ell, les primeres implementacions paguen
// la compilacio JIT i la comparacio no es justa.
for (Comptador c : comptadors) mesurar(c, 2, 10_000);
System.out.printf("%-16s | %12s | %10s | %8s | %12s%n",
"Implementacio", "Resultat", "Perduts", "ms", "inc/ms");
System.out.println("-----------------|--------------|------------|----------|-------------");
for (Comptador plantilla : comptadors) {
// Instancia nova per no arrossegar l'escalfament.
Comptador c = switch (plantilla.nom()) {
case "int simple" -> new ComptadorInsegur();
case "volatile" -> new ComptadorVolatile();
case "synchronized" -> new ComptadorSincronitzat();
default -> new ComptadorAmbLock();
};
long ns = mesurar(c, FILS, PER_FIL);
long ms = ns / 1_000_000;
long perduts = ESPERAT - c.valor();
System.out.printf("%-16s | %12d | %10d | %8d | %12d%s%n",
c.nom(), c.valor(), perduts, ms,
ms == 0 ? 0 : c.valor() / ms,
perduts == 0 ? "" : " <-- INCORRECTE");
}
}
}Sortida orientativa:
Implementacio | Resultat | Perduts | ms | inc/ms
-----------------|--------------|------------|----------|-------------
int simple | 1284471 | 2715529 | 31 | 41434 <-- INCORRECTE
volatile | 1893204 | 2106796 | 142 | 13332 <-- INCORRECTE
synchronized | 4000000 | 0 | 213 | 18779
ReentrantLock | 4000000 | 0 | 205 | 19512Anàlisi:
int simpleés el més ràpid i el més incorrecte. Va ràpid precisament perquè no sincronitza res: cada nucli treballa sobre la seva memòria cau. Velocitat sense correcció no val res.volatileés més lent i continua fallant. El pitjor dels mons: paga les barreres de memòria a cada accés i no guanya atomicitat. És la demostració pràctica de l'apartat 6.synchronizediReentrantLockdonen el resultat exacte i tenen temps gairebé idèntics. Des de Java 6,synchronizedestà tan optimitzat comReentrantLock; l'elecció és d'expressivitat, no de rendiment.- Els quatre milions són exactes, sempre. Repeteix el programa:
synchronizediReentrantLockno fallen mai. La correcció no és probabilística.
Solució a l'Exercici 2
package com.nexussoftware.bibliotech.servei;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
public class ReservaDobleSala {
/** Sala amb pany intrinsec i pany explicit, per a les tres versions. */
static class SalaReunions {
final String id;
final Object pany = new Object();
final ReentrantLock lock = new ReentrantLock();
int reserves = 0;
SalaReunions(String id) { this.id = id; }
}
// ---------- 1. VERSIO AMB INTERBLOQUEIG ----------
static void reservarMalament(SalaReunions a, SalaReunions b) {
synchronized (a.pany) {
dormir(100); // amplia la finestra de la fallada
synchronized (b.pany) {
a.reserves++;
b.reserves++;
}
}
}
// ---------- 2. ORDRE GLOBAL D'ADQUISICIO ----------
/**
* Els panys es prenen SEMPRE en ordre creixent d''id'.
* Sense espera circular no pot haver-hi interbloqueig: es una
* garantia estructural, no una probabilitat.
*/
static void reservarBe(SalaReunions a, SalaReunions b) {
SalaReunions primera = a.id.compareTo(b.id) <= 0 ? a : b;
SalaReunions segona = (primera == a) ? b : a;
synchronized (primera.pany) {
dormir(1);
synchronized (segona.pany) {
a.reserves++;
b.reserves++;
}
}
}
// ---------- 3. TRYLOCK AMB RETROCES ALEATORI ----------
static int reservarAmbTryLock(SalaReunions a, SalaReunions b, int maxIntents)
throws InterruptedException {
for (int intent = 1; intent <= maxIntents; intent++) {
boolean tincA = false, tincB = false;
try {
tincA = a.lock.tryLock(50, TimeUnit.MILLISECONDS);
if (tincA) {
tincB = b.lock.tryLock(50, TimeUnit.MILLISECONDS);
}
if (tincA && tincB) {
a.reserves++;
b.reserves++;
return intent; // exit: retornem l'intent
}
} finally {
if (tincB) b.lock.unlock();
if (tincA) a.lock.unlock();
}
// Retroces ALEATORI: sense ell, dos fils en fase reintentarien
// eternament alhora (livelock, apartat 12).
TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextInt(5, 40));
}
return -1; // no s'ha aconseguit
}
// ---------- DEMOSTRACIO ----------
public static void main(String[] args) throws InterruptedException {
// --- 1. Provocar l'interbloqueig ---
System.out.println("=== 1. VERSIO AMB INTERBLOQUEIG ===");
SalaReunions s1 = new SalaReunions("SALA-A");
SalaReunions s2 = new SalaReunions("SALA-B");
Thread t1 = new Thread(() -> reservarMalament(s1, s2), "fil-A-B");
Thread t2 = new Thread(() -> reservarMalament(s2, s1), "fil-B-A");
t1.start(); t2.start();
t1.join(3000); t2.join(3000);
if (t1.isAlive() || t2.isAlive()) {
System.out.println(" INTERBLOQUEJAT. t1=" + t1.getState()
+ " t2=" + t2.getState());
System.out.println(" (jstack diria: Found one Java-level deadlock)");
} else {
System.out.println(" Aquesta vegada no ha passat; reintenta-ho. "
+ "La intermitencia es exactament el problema.");
}
// --- 2. Ordre global: 1.000 intents amb 8 fils ---
System.out.println();
System.out.println("=== 2. ORDRE GLOBAL D'ADQUISICIO ===");
SalaReunions a = new SalaReunions("SALA-A");
SalaReunions b = new SalaReunions("SALA-B");
Thread[] fils = new Thread[8];
for (int i = 0; i < 8; i++) {
final boolean directe = (i % 2 == 0);
fils[i] = new Thread(() -> {
for (int k = 0; k < 125; k++) {
if (directe) reservarBe(a, b);
else reservarBe(b, a); // sentit oposat
}
}, "reservador-" + i);
fils[i].start();
}
for (Thread f : fils) f.join(20_000);
boolean alguViu = false;
for (Thread f : fils) alguViu |= f.isAlive();
System.out.println(" Reserves SALA-A: " + a.reserves);
System.out.println(" Reserves SALA-B: " + b.reserves);
System.out.println(" Algun fil bloquejat: " + alguViu);
System.out.println(alguViu ? " FALLADA" : " CORRECTE: 1.000 reserves sense interbloqueig");
// --- 3. tryLock amb retroces ---
System.out.println();
System.out.println("=== 3. TRYLOCK AMB RETROCES ALEATORI ===");
SalaReunions c = new SalaReunions("SALA-C");
SalaReunions d = new SalaReunions("SALA-D");
AtomicInteger reintentsTotals = new AtomicInteger();
AtomicInteger fracassos = new AtomicInteger();
Thread[] ts = new Thread[4];
for (int i = 0; i < 4; i++) {
final boolean directe = (i % 2 == 0);
ts[i] = new Thread(() -> {
for (int k = 0; k < 50; k++) {
try {
int intents = directe
? reservarAmbTryLock(c, d, 10)
: reservarAmbTryLock(d, c, 10);
if (intents < 0) fracassos.incrementAndGet();
else reintentsTotals.addAndGet(intents - 1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "trylock-" + i);
ts[i].start();
}
for (Thread t : ts) t.join();
System.out.println(" Reserves SALA-C : " + c.reserves);
System.out.println(" Reserves SALA-D : " + d.reserves);
System.out.println(" Reintents totals : " + reintentsTotals.get());
System.out.println(" Fracassos : " + fracassos.get());
System.out.println(" (els reintents son el preu de no imposar un ordre)");
}
static void dormir(long ms) {
try { TimeUnit.MILLISECONDS.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}Sortida orientativa:
=== 1. VERSIO AMB INTERBLOQUEIG ===
INTERBLOQUEJAT. t1=BLOCKED t2=BLOCKED
(jstack diria: Found one Java-level deadlock)
=== 2. ORDRE GLOBAL D'ADQUISICIO ===
Reserves SALA-A: 1000
Reserves SALA-B: 1000
Algun fil bloquejat: false
CORRECTE: 1.000 reserves sense interbloqueig
=== 3. TRYLOCK AMB RETROCES ALEATORI ===
Reserves SALA-C : 200
Reserves SALA-D : 200
Reintents totals : 37
Fracassos : 0La comparació entre 2 i 3 és la moralitat: l'ordre global no va necessitar ni un reintent, ni una espera, ni una línia extra de codi en temps d'execució. El tryLock va funcionar però va pagar 37 reintents i un munt de complexitat. Quan puguis ordenar els panys, ordena'ls.
Solució a l'Exercici 3
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Fitxa;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Cache de fitxes segura per a diversos fils, optimitzada per a lectura.
*
* POLITICA: tot l'estat esta protegit per 'pany'.
* Les consultes que encerten fan servir el pany de LECTURA (compartit);
* nomes les fallades escalen al d'ESCRIPTURA (exclusiu).
*/
public class CacheFitxesSegura {
private final ReentrantReadWriteLock pany = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock lectura = pany.readLock();
private final ReentrantReadWriteLock.WriteLock escriptura = pany.writeLock();
/** Protegit per 'pany'. */
private final Map<String, Fitxa> cache = new HashMap<>();
/** Protegits per 'pany'. */
private long encerts = 0;
private long fallades = 0;
public Fitxa obtenir(String isbn) {
// FASE 1: intent optimista amb pany COMPARTIT.
// Diversos fils poden ser aqui alhora, que es el 96% del transit.
lectura.lock();
try {
Fitxa f = cache.get(isbn);
if (f != null) {
// COMPTE: 'encerts++' sota el pany de LECTURA seria una
// cursa: diversos lectors simultanis farien
// llegir-modificar-escriure alhora. Ho comptem a la fase 2.
return f;
}
} finally {
lectura.unlock();
}
// FASE 2: fallada. Calculem FORA de tot pany (regla 2 de
// l'apartat 9: mai operacions lentes dins del bloqueig).
Fitxa calculada = calcular(isbn);
// FASE 3: publicacio amb pany EXCLUSIU.
escriptura.lock();
try {
// Re-comprovem: entre la fase 1 i la 3 un altre fil ha pogut
// calcular la mateixa fitxa. putIfAbsent conserva la primera
// i mante la coherencia del recompte.
Fitxa jaPosada = cache.putIfAbsent(isbn, calculada);
if (jaPosada != null) {
encerts++; // un altre fil ens ha guanyat la cursa
return jaPosada;
}
fallades++;
return calculada;
} finally {
escriptura.unlock();
}
}
public void invalidar(String isbn) {
escriptura.lock();
try { cache.remove(isbn); }
finally { escriptura.unlock(); }
}
public int mida() {
lectura.lock();
try { return cache.size(); }
finally { lectura.unlock(); }
}
public String estadistiques() {
lectura.lock();
try {
long total = encerts + fallades;
return String.format("encerts=%d fallades=%d taxa=%.1f%%",
encerts, fallades, total == 0 ? 0.0 : 100.0 * encerts / total);
} finally {
lectura.unlock();
}
}
/** Simula el cost real de construir una fitxa (consulta + formatatge). */
private Fitxa calcular(String isbn) {
try { TimeUnit.MILLISECONDS.sleep(20); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return new Fitxa(isbn, "Titol de " + isbn, "Autor", 3);
}
// ---------- VERSIO DE CONTRAST: un sol pany exclusiu ----------
public static class CacheFitxesSincronitzada {
private final Map<String, Fitxa> cache = new HashMap<>();
// TOT el metode sincronitzat, INCLOS el calcul de 20 ms.
// Es l'error de la regla 2 de l'apartat 9, a proposit.
public synchronized Fitxa obtenir(String isbn) {
Fitxa f = cache.get(isbn);
if (f == null) {
try { TimeUnit.MILLISECONDS.sleep(20); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
f = new Fitxa(isbn, "Titol de " + isbn, "Autor", 3);
cache.put(isbn, f);
}
return f;
}
public synchronized int mida() { return cache.size(); }
}
// ---------- PROVA ----------
public static void main(String[] args) throws InterruptedException {
final int FILS = 10;
final int CONSULTES = 2_000;
final int ISBNS = 50;
// --- Versio amb ReadWriteLock ---
CacheFitxesSegura rw = new CacheFitxesSegura();
long msRw = executar(FILS, CONSULTES, ISBNS, isbn -> rw.obtenir(isbn));
System.out.println("=== ReadWriteLock ===");
System.out.println(" Temps : " + msRw + " ms");
System.out.println(" Entrades : " + rw.mida());
System.out.println(" " + rw.estadistiques());
// --- Versio amb synchronized total ---
CacheFitxesSincronitzada sy = new CacheFitxesSincronitzada();
long msSy = executar(FILS, CONSULTES, ISBNS, isbn -> sy.obtenir(isbn));
System.out.println();
System.out.println("=== synchronized total ===");
System.out.println(" Temps : " + msSy + " ms");
System.out.println(" Entrades : " + sy.mida());
System.out.println();
System.out.printf("Relacio: %.1fx a favor del ReadWriteLock%n",
(double) msSy / msRw);
}
interface Consulta { Fitxa obtenir(String isbn); }
static long executar(int fils, int consultes, int isbns, Consulta c)
throws InterruptedException {
Thread[] ts = new Thread[fils];
long inici = System.nanoTime();
for (int i = 0; i < fils; i++) {
ts[i] = new Thread(() -> {
for (int k = 0; k < consultes; k++) {
c.obtenir("978-" + String.format("%010d", k % isbns));
}
}, "consultor-" + i);
ts[i].start();
}
for (Thread t : ts) t.join();
return (System.nanoTime() - inici) / 1_000_000;
}
}Sortida orientativa:
=== ReadWriteLock ===
Temps : 152 ms
Entrades : 50
encerts=19 fallades=50 taxa=27.5%
=== synchronized total ===
Temps : 1043 ms
Entrades : 50
Relacio: 6.9x a favor del ReadWriteLockAnàlisi del resultat:
- La diferència de 7× no ve del
ReadWriteLocken si, sinó de treure el càlcul fora del pany. A la versió sincronitzada, els 20 ms de càlcul passen amb el pany exclusiu pres, així que les 50 fallades inicials es serialitzen: 50 × 20 ms = 1 segon com a mínim, amb nou fils aturats esperant. És la regla 2 de l'apartat 9 costant un factor de set. - El
ReadWriteLockaporta la segona meitat: un cop plena la memòria cau, les 19.950 consultes restants són encerts que s'atenen en paral·lel amb el pany compartit. Amb un pany exclusiu es serialitzarien també, encara que cadascuna durés microsegons. - Els 19 "encerts" comptats a la fase 3 són la cursa benigna: dos fils van fallar sobre el mateix ISBN alhora i tots dos el van calcular; el
putIfAbsenten conserva un de sol. Es fa feina duplicada però el resultat és correcte. Evitar-ho completament requeriria bloquejar durant el càlcul —justament el que volem evitar— o unConcurrentHashMap.computeIfAbsent, que ho resol elegantment i és 08-06.
Conclusió
Aquesta era la lliçó que sosté el mòdul, i amb ella tanques el problema que vas obrir a 08-01.
Saps per què falla el comptador. comptador++ no és una operació: són tres instruccions de bytecode —getfield, iadd, putfield— i qualsevol entrellaçat entre elles perd feina. Reconeixes els dos patrons canònics —llegir-modificar-escriure i comprovar-després-actuar— i saps que veure qualsevol d'ells sobre estat compartit és veure un bug. Saps quines operacions són atòmiques en Java i quines no, inclòs el cas de long i double, l'escriptura dels quals l'especificació permet partir en dues meitats.
Coneixes el model de memòria de Java, que és el que separa entendre la concurrència d'aplicar-la per superstició. Saps que la memòria no és una llibreta compartida: hi ha memòries cau per nucli, el compilador reordena emparat en una garantia que només val dins d'un fil, i la CPU també reordena. D'aquí la conseqüència que més costa acceptar i que vas veure executant-se: un fil pot escriure aturat = true i un altre no assabentar-se'n mai, perquè el JIT va treure la lectura fora del bucle. I saps que la garantia real s'expressa amb la relació happens-before, amb les seves regles —ordre del programa, monitor, volatile, start, join, camps final, transitivitat—, i amb la pregunta que cal fer-se davant de cada dada compartida: "quina regla happens-before connecta aquestes dues accions?". Si no hi ha resposta, hi ha una cursa de dades.
Saps exactament què fa volatile i què no. Garanteix visibilitat i no reordenació, i fa atòmiques les lectures i escriptures de 64 bits. No fa atòmic un increment. Els dos exemples ho demostren de forma incontestable: el marcador d'aturada que sense volatile penja el programa per sempre, i el comptador que amb volatile continua perdent el 40 % dels increments —més lent i igual d'incorrecte—.
Domines synchronized. El monitor intrínsec que tot objecte porta a dins, l'alliberament automàtic fins i tot davant d'excepcions, la reentrada que fa possible l'herència, i les dues garanties que dona alhora: exclusió mútua i visibilitat, aquesta última tan important com la primera i sistemàticament oblidada —per això el getter també va sincronitzat—. Coneixes les tres formes i per què la del bloc amb pany privat i final guanya a les altres dues: granularitat i no exposar el pany al món. I coneixes els paranys: el mètode d'instància i l'estàtic fan servir monitors diferents, i un String literal o un Integer petit com a pany és un objecte compartit amb tota la JVM.
Tens les cinc regles de què sincronitzar: secció crítica curta, mai E/S dins del bloqueig, mai codi aliè amb el pany a la mà —copiar a dins, notificar a fora—, no sincronitzar el que no es comparteix, i documentar la política amb un /** Protegit per 'pany'. */ que costa una línia i estalvia hores.
Saps provocar i resoldre un interbloqueig. Les quatre condicions de Coffman, de les quals només l'espera circular està a la teva mà; l'exemple reproduïble de dues transferències en sentits oposats, amb els dos fils en BLOCKED per sempre, sense excepció i sense consum de CPU; i les tres solucions per ordre de preferència: ordre global d'adquisició —garantia absoluta, cost zero, la resposta correcta gairebé sempre—, tryLock amb termini i retrocés aleatori quan no pots ordenar, i un sol pany més gruixut quan la contenció ho permet. Amb les dues patologies veïnes: la inanició, que es combat amb seccions curtes i, si de veritat cal, amb un pany equitatiu car; i el livelock, que es distingeix de l'interbloqueig perquè consumeix CPU al 100 % i jstack no el declara.
Coneixes la resta de la caixa d'eines: ReentrantLock amb el seu lock(); try { } finally { unlock(); } obligatori, i les cinc coses que aporta —tryLock, termini, lockInterruptibly, equitat i diverses condicions—, amb la recomanació de fer servir synchronized per defecte i passar a Lock només quan necessitis una d'elles. Condition com a substitut de wait/notify, amb l'avantatge decisiu de tenir diverses cues d'espera per pany, que fa segur el signal() i elimina el despertar massiu de notifyAll. I ReadWriteLock, amb la seva taula de compatibilitat, la proporció d'almenys 5:1 que el justifica, i la regla que s'oblida: la degradació està permesa, la promoció és un interbloqueig instantani amb si mateix.
I, sobretot, tens les dues estratègies que guanyen a totes les anteriors. La immutabilitat: un objecte amb tots els seus camps final que no deixa escapar this és segur per a qualsevol nombre de fils, sense panys, per sempre —i els record de 04-07 ho són per construcció, amb el detall del constructor compacte que copia les col·leccions—. I el confinament: la millor sincronització és no compartir, ja sigui a la pila, amb ThreadLocal —amb el seu avís sobre pools i fuites— o dividint la feina i combinant al final. Amb la jerarquia que hauries de recórrer sempre de dalt a baix: no compartir, compartir immutable, volatile, atòmic, col·lecció concurrent, pany.
BiblioTech ja suporta dos empleats alhora. CatalegSegur protegeix les seves tres estructures amb un ReadWriteLock que deixa passar tots els lectors simultàniament i serialitza només les altes, i retorna còpies en lloc de les seves col·leccions internes. RegistrePrestecsSegur manté l'invariant entre perId i perEmpleat sota un únic pany —perquè un invariant que abasta dues estructures no es pot sostenir amb peces independents—, i exposa registrarSiHiHaLloc com a operació composta atòmica, perquè obligar el client a escriure if (actius() < MAX) registrar(p) seria regalar-li una cursa. Vuit fils i quaranta mil operacions donen el resultat exacte, deu vegades de deu. El cas C de 08-01 està tancat.
Però mira com hi has arribat: panys a mà, finally que no es poden oblidar, ordres d'adquisició que cal documentar i respectar, i —el més car de tot— un fil creat a mà per cada tasca. Els 200 avisos de BiblioTech continuen necessitant 200 fils, cadascun amb el seu megabyte de pila. Escriure concurrència correcta a aquest nivell és possible, però és artesanal i fràgil, i tota la indústria fa vint anys que no ho fa així.
A la propera lliçó, Utilitats de Concurrència, puges per fi de nivell. Veuràs ExecutorService i les fàbriques d'Executors —pools fixos, elàstics, d'un sol fil i programats—, amb la taula de quan fer servir cadascun i l'advertiment sobre les cues il·limitades que tomben aplicacions; com construir un ThreadPoolExecutor a mida amb la seva ThreadFactory per posar nom als fils i la seva política de rebuig; el cicle de vida correcte d'un executor amb shutdown, shutdownNow i awaitTermination; Callable i Future, que per fi permeten a una tasca retornar un resultat i propagar la seva excepció, amb get() blocant i amb termini, i cancel(true) que no és altra cosa que la interrupció de 08-02; ScheduledExecutorService per als avisos periòdics de venciment; i els sincronitzadors —CountDownLatch, CyclicBarrier, Semaphore— que substitueixen les coordinacions a mà per peces provades. En acabar, els dos-cents avisos de BiblioTech s'enviaran amb un pool acotat, amb barra de progrés i amb cancel·lació neta, en uns pocs segons i amb vuit fils en lloc de dos-cents.
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
