De la xarxa tornem a casa: dins de cada servei de PideYa hi ha desenes de fils atenent peticions alhora, i tots comparteixen la mateixa memòria. Aquí les fallades no són timeouts visibles sinó condicions de carrera: bugs que apareixen una vegada cada mil execucions i desapareixen quan hi poses un log. A 02-02 vam mostrar el double-checked locking del Singleton gairebé com un encanteri; aquesta lliçó en dona per fi el fonament, i amb ell un catàleg de patrons perquè els fils cooperin sense trepitjar-se. La distribució entre processos va quedar coberta a la lliçó anterior; aquí tot passa dins d'una JVM.
Contingut
- El problema: estat compartit mutable
- El double-checked locking, ara amb fonament
- Immutabilitat com a patró
- Confinament d'estat
- Producer-Consumer amb
BlockingQueue - Thread Pool / Worker amb
ExecutorService - Future, Promise i
CompletableFuture - Read-Write Lock
- Active Object (menció) i virtual threads de Java 21
- Guia: quan usar què
El problema: estat compartit mutable
Dos fils executen comptadorComandes++ alhora. Aquesta línia són tres operacions (llegir, sumar, escriure); si s'intercalen, un increment es perd. Pitjor encara: encara que no s'intercalin, un fil pot no veure el que va escriure un altre, perquè cada nucli fa memòria cau i el compilador reordena instruccions. El Java Memory Model només garanteix visibilitat entre fils quan hi ha una relació happens-before: la creen synchronized, volatile, els java.util.concurrent i l'arrencada/join de fils.
L'equació del perill és: estat compartit + mutabilitat + concurrència. Tots els patrons d'aquesta lliçó eliminen almenys un factor:
| Patró | Factor que elimina |
|---|---|
| Immutabilitat | La mutabilitat |
| Confinament | El compartir |
| Producer-Consumer, Active Object | L'accés simultani (serialitza per cues) |
| Locks (inclòs el Read-Write) | L'accés simultani (exclou) |
El double-checked locking, amb fonament
Recordem el Singleton mandrós de ConfiguracioPideYa:
public class ConfiguracioPideYa {
private static volatile ConfiguracioPideYa instancia; // volatile: imprescindible
public static ConfiguracioPideYa getInstancia() {
if (instancia == null) { // 1a comprovacio, sense lock (rapida)
synchronized (ConfiguracioPideYa.class) {
if (instancia == null) { // 2a comprovacio, amb lock
instancia = new ConfiguracioPideYa();
}
}
}
return instancia;
}
}Ara podem explicar el perquè de cada peça. Sense volatile, la JVM pot reordenar: publicar la referència instancia abans d'acabar el constructor. Un altre fil passaria la primera comprovació i usaria un objecte a mig construir — el bug fantasma per excel·lència. volatile prohibeix aquest reordenament i garanteix la visibilitat (happens-before entre l'escriptura i les lectures). La primera comprovació evita pagar el lock en el 99,9 % de crides; la segona evita la doble creació entre fils que esperaven el lock. I la moralitat de 02-02 continua en peu: el holder idiom o un enum aconsegueixen el mateix sense escriure res de tot això.
Immutabilitat com a patró
L'objecte immutable és el patró de concurrència més barat: allò que no canvia es pot compartir entre mil fils sense cap lock. Java ho ha anat facilitant:
// Un record es final, amb camps finals: immutable per construccio
public record LiniaComanda(String platId, int quantitat, BigDecimal preu) { }
public final class Comanda {
private final List<LiniaComanda> linies;
private Comanda(Builder b) {
// List.copyOf: copia immutable — ni el builder ni ningu no pot mutar-la despres
this.linies = List.copyOf(b.linies);
}
public List<LiniaComanda> getLinies() { return linies; } // segur: es immutable
}Aquell List.copyOf que vam posar al Comanda.Builder de 02-05 per "bones pràctiques" era, en realitat, un patró de concurrència: la còpia defensiva que fa la Comanda compartible. I si la comanda "canvia"? No es muta: es crea una altra comanda (com feia clonar() a Prototype, 02-06) o, millor, es registra el canvi com a esdeveniment — la connexió amb l'Event Sourcing de 06-01. Els objectes que viatjaven pels topics de 06-03 havien de ser immutables per aquesta mateixa raó.
Confinament d'estat
Si una dada la toca un sol fil, no necessita sincronització encara que sigui mutable. Maneres de confinar:
- Confinament a pila: variables locals; cada fil té la seva pila. Un
StringBuilderlocal no necessita locks. - Confinament per fil:
ThreadLocal<T>dona a cada fil la seva pròpia còpia (així desa Spring la transacció actual). - Confinament per disseny: un únic fil posseeix l'estructura i els altres li demanen coses per missatges — que és exactament el patró següent.
Producer-Consumer amb BlockingQueue
Les comandes de cuina de PideYa: els fils web que confirmen comandes (productors) no han d'esperar que la cuina processi; la dipositen en una cua i continuen.
BlockingQueue<ComandaCuina> cuaCuina = new LinkedBlockingQueue<>(100); // capacitat acotada
// Productor (fil web): si la cua es plena, put() BLOQUEJA → contrapressio
public void confirmarComanda(Comanda c) throws InterruptedException {
cuaCuina.put(new ComandaCuina(c.getId(), c.getLinies()));
}
// Consumidor (fil de cuina): take() bloqueja fins que hi hagi comanda
Runnable cuiner = () -> {
while (!Thread.currentThread().isInterrupted()) {
try {
ComandaCuina cc = cuaCuina.take();
prepararComanda(cc); // nomes AQUEST fil toca l'estat de la comanda
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restaurar el flag i sortir
}
}
};La BlockingQueue encapsula tota la sincronització: put i take són segurs i bloquegen quan toca. La capacitat acotada (100) és la decisió de disseny important: si la cuina no dona l'abast, els productors es frenen (contrapressió) en lloc d'esgotar la memòria. És la versió intra-procés de les cues de missatges de 06-03: mateixa intenció, sense xarxa ni broker — i sense les seves garanties de durabilitat: si el procés mor, la cua en memòria es perd.
Thread Pool / Worker amb ExecutorService
Crear un fil per tasca és car (memòria i arrencada) i perillós (un pic de trànsit crea deu mil fils i tomba la JVM). El patró Thread Pool reutilitza un conjunt fix de workers que consumeixen tasques d'una cua interna — és un Producer-Consumer empaquetat:
// Dimensionat per al processament de comandes:
// tasques amb I/O (BD, xarxa) → mes fils que nuclis; tasques de pur CPU → ~num de nuclis
int nuclis = Runtime.getRuntime().availableProcessors();
ExecutorService poolComandes = Executors.newFixedThreadPool(nuclis * 4);
poolComandes.submit(() -> processarComanda(comanda)); // encuar tasca, no crear filRegla de dimensionat clàssica: fils ≈ nuclis × (1 + tempsEspera/tempsCpu). El processament d'una comanda de PideYa passa ~75 % del temps esperant la BD i Pagaments, d'aquí el × 4. Un pool per a CPU pura (calcular rutes de repartiment) es quedaria en nuclis. I fixa-t'hi: tenir pools separats per a "crides a Pagaments" i "crides a Catàleg" és exactament el bulkhead de 06-03, versió local.
Future, Promise i CompletableFuture
Un Future<T> és un rebut: "el resultat arribarà; mentrestant, continua amb la teva vida". Un promise és el costat escrivible del rebut. En Java tots dos papers els compleix CompletableFuture, que a més permet compondre passos asíncrons. El checkout de PideYa: cobrar i reservar estoc no depenen l'un de l'altre — al monòlit seqüencial sumaven les seves latències; en paral·lel, només costa la més gran:
CompletableFuture<ResultatCobrament> cobrament =
CompletableFuture.supplyAsync(() -> passarella.cobrar(comanda, targeta), poolComandes);
CompletableFuture<Reserva> estoc =
CompletableFuture.supplyAsync(() -> magatzem.reservar(comanda.getLinies()), poolComandes);
// Combinar tots dos resultats quan TOTS DOS acabin
CompletableFuture<Confirmacio> confirmacio =
cobrament.thenCombine(estoc, (r, s) -> confirmar(comanda, r, s))
.orTimeout(3, TimeUnit.SECONDS) // el timeout de 06-03, local
.exceptionally(ex -> compensarIRebutjar(comanda, ex));Llegeix-ho com una canonada declarativa: supplyAsync llança cada tasca al pool, thenCombine espera les dues i combina, orTimeout acota l'espera i exceptionally és el pla B (que aquí ha de compensar: si el cobrament va anar bé però l'estoc va fallar, cal reemborsar — la lògica de saga de 06-02 en miniatura). Res no bloqueja el fil cridador: els callbacks s'encadenen com els Decorator encadenaven comportament.
Read-Write Lock
La carta de PideYa (el Composite de 03-04) es llegeix en cada petició de cada client i s'escriu unes poques vegades al dia. Un synchronized normal serialitza també les lectures entre si — un malbaratament, perquè llegir en paral·lel és inofensiu. El Read-Write Lock distingeix:
public class CartaCompartida {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private SeccioCarta arrel;
public Menu cercarMenu(String id) {
lock.readLock().lock(); // N lectors poden entrar ALHORA
try { return arrel.cercar(id); }
finally { lock.readLock().unlock(); }
}
public void actualitzar(SeccioCarta novaArrel) {
lock.writeLock().lock(); // l'escriptor entra SOL, sense lectors
try { this.arrel = novaArrel; }
finally { lock.writeLock().unlock(); }
}
}Molts lectors simultanis, escriptors en exclusiva. Alternativa encara millor quan les escriptures són tan rares: publicar una carta immutable i que actualitzar reemplaci la referència (volatile o AtomicReference) — copy-on-write, que combina els patrons 3 i 8 d'aquesta lliçó i no bloqueja ningú.
Active Object i virtual threads
Active Object (POSA): un objecte amb el seu propi fil i la seva pròpia cua de peticions; els mètodes públics no executen, encuen, i retornen futures. És Command + Producer-Consumer + Future en un paquet: així es podria implementar la CentralRepartiment perquè tot el seu estat quedi confinat a un fil. Ho deixem en menció: la combinació de peces ja la coneixes.
Virtual threads (Java 21): fils baratíssims gestionats per la JVM (milions, no milers), que alliberen el fil de plataforma en bloquejar-se en I/O. Què canvien: l'estil "un fil per petició" amb codi bloquejant seqüencial torna a ser viable — menys necessitat d'encadenar CompletableFuture per escalar (continuen valent per paral·lelitzar, com el cobrament+estoc). Què no canvien: res de la correcció — les carreres, la visibilitat, els locks i la immutabilitat continuen exactament igual. Els patrons d'aquesta lliçó no caduquen amb ells.
Guia: quan usar què
| Situació a PideYa | Patró | Eina Java |
|---|---|---|
| Dades que viatgen entre fils | Immutabilitat | record, List.copyOf, final |
| Estat auxiliar per petició | Confinament | Variables locals, ThreadLocal |
| Feina desacoblada amb contrapressió (comandes de cuina) | Producer-Consumer | BlockingQueue acotada |
| Moltes tasques, fils controlats | Thread Pool | ExecutorService dimensionat |
| Passos independents en paral·lel (cobrament + estoc) | Future/Promise | CompletableFuture.thenCombine |
| Es llegeix molt, s'escriu poc (carta) | Read-Write Lock / copy-on-write | ReentrantReadWriteLock, AtomicReference |
| Estat complex amb un sol amo | Active Object | Fil propi + cua + futures |
| Milers de peticions bloquejants d'I/O | Virtual threads | Executors.newVirtualThreadPerTaskExecutor() |
| Comptador o referència solta compartida | (atòmics) | AtomicInteger, LongAdder |
Errors Comuns i Consells
- Sincronitzar "per si de cas" o no sincronitzar "perquè funciona": tots dos neixen de no raonar el model de memòria. Pregunta sistemàtica: quins fils toquen aquesta dada i quina relació happens-before els ordena? Si no saps respondre, hi ha un bug latent.
- Double-checked locking sense
volatile: compila, passa els tests i falla en producció sota càrrega. Si necessites inicialització mandrosa, prefereix el holder idiom. - Cues sense acotar:
new LinkedBlockingQueue<>()sense capacitat converteix un pic de trànsit en unOutOfMemoryErrordiferit. Acota i decideix què passa quan s'ompli (bloquejar, rebutjar, descartar). - Bloquejar dins d'un
CompletableFutureambjoin()/get()al mig de la cadena: anul·la l'asincronia i pot provocar deadlocks de pool. Compondre, no esperar. - Compartir el pool per a tot: les tasques d'I/O lentes de Pagaments ofeguen les ràpides del catàleg. Pools separats per tipus de feina (bulkhead local).
- Locks niats en ordre diferent: el fil A pren lock1→lock2, el fil B pren lock2→lock1: deadlock. Estableix un ordre global d'adquisició o redissenya per no niar.
- Empassar-se la
InterruptedException: uncatchbuit impedeix aturar el sistema netament. Restaura el flag (Thread.currentThread().interrupt()) i acaba.
Exercicis
- Caça la carrera.
ComptadorComandesDiatéprivate int total;i el mètodepublic void incrementar() { total++; }, cridat des dels fils web. Explica els dos problemes diferents (atomicitat i visibilitat) i dona dues solucions: una amb lock i una sense lock. - Dimensiona el pool. El servei de notificacions envia emails: cada enviament usa ~5 ms de CPU i espera ~195 ms la xarxa del proveïdor. La màquina té 8 nuclis. Calcula la mida de pool raonable amb la fórmula de la lliçó i explica quin patró de 06-03 convé posar davant del pool.
- Paral·lelitza el checkout. En confirmar una comanda cal: (a) cobrar, (b) reservar estoc, (c) calcular punts de fidelització — les tres independents entre si — i (d) amb els resultats de les tres, emetre la confirmació. Escriu la composició amb
CompletableFuturei afegeix-hi un timeout global de 2 segons.
Solucions
-
Atomicitat:
total++són tres passos; dos fils poden llegir el mateix valor i perdre's un increment. Visibilitat: sense happens-before, un fil pot no veure mai les escriptures d'un altre (ivolatilenomés arreglaria la visibilitat, no l'atomicitat). Amb lock:public synchronized void incrementar() { total++; }(i tambésynchronizedal getter). Sense lock:private final AtomicInteger total = new AtomicInteger(); public void incrementar() { total.incrementAndGet(); }— oLongAddersi la contenció és alta. -
fils ≈ 8 × (1 + 195/5) = 8 × 40 = 320fils. Amb tanta espera d'I/O el pool surt enorme — senyal que els virtual threads hi encaixarien encara millor. Davant del pool: una cua productor-consumidor acotada amb contrapressió, i conceptualment l'enviament ja venia d'un broker (06-03), amb reintents i DLQ per als emails que fallin. -
La composició queda així:
var cobrament = CompletableFuture.supplyAsync(() -> passarella.cobrar(comanda, targeta), pool); var estoc = CompletableFuture.supplyAsync(() -> magatzem.reservar(comanda.getLinies()), pool); var punts = CompletableFuture.supplyAsync(() -> fidelitzacio.calcularPunts(comanda), pool); CompletableFuture<Confirmacio> conf = cobrament.thenCombine(estoc, ParPagamentEstoc::new) .thenCombine(punts, (par, pts) -> emetreConfirmacio(comanda, par.cobrament(), par.reserva(), pts)) .orTimeout(2, TimeUnit.SECONDS) .exceptionally(ex -> compensarIRebutjar(comanda, ex));thenCombineen cascada ajunta els tres resultats (amb un record auxiliarParPagamentEstoc);orTimeoutacota l'operació completa iexceptionallycentralitza la compensació (reemborsar/alliberar allò que sí que es va arribar a executar).
Conclusió
Hem baixat al nivell on els bugs no donen la cara: l'estat compartit mutable. El catàleg de resposta — immutabilitat, confinament, cues amb contrapressió, pools ben dimensionats, futures compostos, read-write locks, i els virtual threads que abarateixen els fils sense derogar cap regla — reutilitza intencions que ja dominaves: el Builder que copiava llistes era concurrència preventiva, el Producer-Consumer és la cua de missatges sense xarxa, l'Active Object és Command més Future. Amb això es tanca la part tècnica del catàleg modern. Queda una pregunta diferent, ni de xarxa ni de fils sinó de persones i de temps: com conviuen tots aquests patrons amb el desenvolupament iteratiu, els sprints i el "no ho necessitaràs"? Ho veiem a Patrons de Disseny en Desenvolupament Àgil.
Curs de Patrons de Disseny de Programari
Mòdul 1: Introducció als Patrons de Disseny
- Què són els Patrons de Disseny?
- Història i Origen dels Patrons de Disseny
- Principis de Disseny: SOLID i Altres Fonaments
- UML Essencial per Entendre Patrons
- Classificació dels Patrons de Disseny
- Avantatges i Desavantatges d'Usar Patrons de Disseny
Mòdul 2: Patrons Creacionals
- Introducció als Patrons Creacionals
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa i Elecció de Patrons Creacionals
Mòdul 3: Patrons Estructurals
- Introducció als Patrons Estructurals
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa i Elecció de Patrons Estructurals
Mòdul 4: Patrons de Comportament
- Introducció als Patrons de Comportament
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa i Elecció de Patrons de Comportament
Mòdul 5: Aplicació de Patrons de Disseny
- Com Seleccionar el Patró Adequat
- Exemples Pràctics d'Ús de Patrons
- Patrons de Disseny en Projectes Reals
- Refactorització Usant Patrons de Disseny
- Antipatrons: Quan els Patrons es Tornen un Problema
Mòdul 6: Patrons de Disseny Avançats
- Patrons de Disseny en Arquitectures Modernes
- Patrons de Disseny en Microserveis
- Patrons de Disseny en Sistemes Distribuïts
- Patrons de Concurrència
- Patrons de Disseny en Desenvolupament Àgil
