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

  1. La condició de cursa al nivell del bytecode
  2. Què és una operació atòmica
  3. El model de memòria de Java
  4. La relació happens-before
  5. volatile: el que garanteix
  6. volatile: el que NO garanteix
  7. synchronized: el monitor intrínsec
  8. Les formes de synchronized
  9. Què sincronitzar i què no
  10. Interbloqueig: l'exemple reproduïble
  11. Les solucions a l'interbloqueig
  12. Inanició i livelock
  13. ReentrantLock davant de synchronized
  14. Condition: substitut de wait/notify
  15. ReadWriteLock: llegir molt, escriure poc
  16. Publicació segura i immutabilitat
  17. Confinament: la millor sincronització és no compartir
  18. BiblioTech: un catàleg segur per a diversos fils
  19. Errors Comuns i Consells
  20. Exercicis

  1. La condició de cursa al nivell del bytecode

comptador++ sembla una operació. No ho és. Compila-ho i mira-ho:

public class Comptador {
    private int valor = 0;
    public void incrementar() {
        valor++;
    }
}
javac Comptador.java
javap -c Comptador
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: return

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

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

  1. 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 long i double, i de qualsevol referència.
  • La lectura i l'escriptura d'un long o double declarat volatile.
  • 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/double no volatile.

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.

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

[main] escrivint aturat = true
[main] EL TREBALLADOR NO SE N'HA ASSABENTAT. Continua viu.

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:

boolean copia = aturat;       // es llegeix UNA vegada
while (!copia) { voltes++; }  // bucle infinit

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:+PrintCompilation per 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:

[main] escrivint aturat = true
[treballador] aturat despres de 1847239104 voltes

S'atura immediatament. Aquesta paraula clau és la diferència entre un programa correcte i un que es penja.

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

  1. volatile: el que garanteix

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

  1. L'escriptura no depèn del valor actual (o hi ha un únic escriptor).
  2. No forma part d'un invariant juntament amb altres variables.
  3. 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.

  1. volatile: el que NO garanteix

Aquí hi ha la confusió més estesa del tema, i convé destruir-la amb una demostració.

volatile NO 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 810553

Continua 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
No reordenació
Atomicitat de lectura/escriptura simple Sí (incl. long/double)
Atomicitat de llegir-modificar-escriure No
Exclusió mútua d'un bloc No No
Pot provocar interbloqueig No 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 dades

volatile protegeix la variable, no l'objecte. Per al contingut calen panys, una col·lecció concurrent (08-06) o immutabilitat.

  1. synchronized: el monitor intrínsec

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

synchronized (objecte) {
    // nomes un fil alhora pot ser aqui
}

Semàntica exacta:

  1. En entrar, el fil adquireix el monitor d'objecte. Si un altre fil el té, queda BLOCKED (08-03) fins que s'alliberi.
  2. 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 de ReentrantLock.
  3. 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 2000000

Exacte, sempre, en totes les execucions. El problema obert des de 08-01 està resolt.

El detall que s'oblida: el getter també va sincronitzat. Si valor() 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'if en lloc del while.

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.

  1. Les formes de synchronized

N'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 Només en classes petites i controlades
synchronized estàtic Classe.class Tot el mètode Només per a estat estàtic
synchronized (panyPrivat) Objecte privat La que decideixis No Preferida

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

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

  1. Exclusió mútua: els recursos no es comparteixen.
  2. Retenir i esperar: un fil en reté un i en demana un altre.
  3. Sense expropiació: no se li pot treure un pany a un fil.
  4. 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:

INTERBLOQUEIG: els dos fils continuen vius i bloquejats.
Estat t1: BLOCKED
Estat t2: BLOCKED

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

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

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

  1. ReentrantLock davant de synchronized

java.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 del try. Si estigués a dins i fallés l'adquisició, el finally faria unlock() d'un pany que no es té: IllegalMonitorStateException. L'ordre correcte és lock(); 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 , 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 , - 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.

  1. Condition: substitut de wait/notify

Un 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() 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.

  1. ReadWriteLock: llegir molt, escriure poc

Un 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 , 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.

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

  1. Inicialitzar-lo des d'un inicialitzador estàtic (la JVM sincronitza la càrrega de classes).
  2. Desar-lo en un camp volatile o en una AtomicReference (08-06).
  3. Desar-lo en un camp final d'un objecte correctament construït.
  4. Desar-lo en un camp protegit per un pany, i llegir-lo amb el mateix pany.
  5. 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 final i no deixa escapar this durant 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:

  1. Té tots els seus camps final.
  2. No exposa cap mètode que canviï el seu estat.
  3. No deixa escapar this al constructor.
  4. 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.

  1. 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 ThreadLocal i 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 seu ThreadLocal pot exposar aquesta dada a la petició de Diego Alonso. Regla: si fas servir ThreadLocal en un pool, neteja'l en un finally.

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 panys

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

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

Executa 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():

  1. ComptadorInsegur: int simple.
  2. ComptadorVolatile: volatile int.
  3. ComptadorSincronitzat: synchronized.
  4. 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:

  1. Un mètode reservarMalament(SalaReunions a, SalaReunions b) que adquireixi els dos panys en l'ordre en què se li passen, amb un sleep(100) entre tots dos, i un main que el interbloquegi de forma reproduïble amb dos fils que reservin en sentits oposats, detectant-ho amb join(3000) + isAlive().
  2. 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.
  3. Un mètode reservarAmbTryLock(...) amb ReentrantLock.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 |        19512

Anà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.
  • synchronized i ReentrantLock donen el resultat exacte i tenen temps gairebé idèntics. Des de Java 6, synchronized està tan optimitzat com ReentrantLock; l'elecció és d'expressivitat, no de rendiment.
  • Els quatre milions són exactes, sempre. Repeteix el programa: synchronized i ReentrantLock no 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         : 0

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

Anàlisi del resultat:

  1. La diferència de 7× no ve del ReadWriteLock en 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.
  2. El ReadWriteLock aporta 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.
  3. 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 putIfAbsent en 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 un ConcurrentHashMap.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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

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

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

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

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

© Copyright 2026. Tots els drets reservats