BiblioTech ja fa coses en paral·lel. Envia dos-cents avisos amb un pool acotat, protegeix el seu catàleg amb estructures concurrents i compta les seves estadístiques sense panys. Però hi ha un gest que es repeteix a tot el codi escrit fins ara i que delata el límit del model: future.get().

Cada vegada que l'aplicació necessita el resultat d'alguna cosa, algú es queda aturat esperant-lo. I si la feina té diversos passos —consultar el catàleg, calcular les multes amb aquell resultat, exportar l'informe— cal fer get() entre cadascun, de manera que el fil que orquestra passa la major part del temps bloquejat. Future sap dir "aquí arribarà un resultat"; no sap dir "quan estigui llest, fes això altre".

CompletableFuture (Java 8) és aquesta resposta. Canvia el model completament: en lloc de preguntar i esperar, declares la cadena sencera per endavant i cada etapa es dispara sola quan l'anterior acaba. Cap fil no espera ningú.

Aquesta és la lliçó de tancament del mòdul. En acabar, BiblioTech tindrà una cadena asíncrona que consulta, calcula i exporta sense bloquejar el menú ni un mil·lisegon, i el mòdul 8 quedarà complet.

Advertiment honest. CompletableFuture té una API àmplia —més de cinquanta mètodes— i és fàcil escriure cadenes il·legibles o, pitjor, cadenes que semblen asíncrones i bloquegen per dins. Aquesta lliçó se centra en el subconjunt que resol el 95 % dels casos i en els paranys que fan que el 5 % restant sigui pitjor que el codi blocant que substitueix.

Contingut

  1. Les quatre limitacions de Future
  2. Què és un CompletableFuture
  3. Creació: supplyAsync, runAsync, completedFuture
  4. L'executor per defecte i per què convé passar el teu
  5. Transformació: thenApply, thenAccept, thenRun
  6. thenApply davant de thenCompose
  7. Les variants ...Async i en quin fil s'executa cada etapa
  8. Combinació: thenCombine
  9. allOf i anyOf
  10. Errors: exceptionally, handle, whenComplete
  11. Com viatja una excepció per la cadena
  12. Temps límit: orTimeout i completeOnTimeout
  13. Completar manualment: adaptar una API de callbacks
  14. Cancel·lació i els seus límits
  15. Bones pràctiques i paranys
  16. BiblioTech: la cadena asíncrona completa
  17. Comparació amb el model reactiu i els fils virtuals
  18. Errors Comuns i Consells
  19. Exercicis

  1. Les quatre limitacions de Future

Future va ser un gran avenç a Java 5, però la seva API té cinc mètodes i cap no permet compondre.

// El problema, en codi real de BiblioTech.
ExecutorService executor = Executors.newFixedThreadPool(4);

Future<Cataleg> f1 = executor.submit(() -> carregarCataleg());
Cataleg c = f1.get();                     // BLOQUEJA. El fil s'atura aqui.

Future<Double> f2 = executor.submit(() -> calcularMultes(c));
double total = f2.get();                  // BLOQUEJA un altre cop.

Future<Path> f3 = executor.submit(() -> exportar(total));
Path informe = f3.get();                  // I un altre cop.

Tres tasques asíncrones, i el fil que orquestra ha estat bloquejat pràcticament tota l'estona. La concurrència és a les tasques, no a la coordinació.

Limitació de Future Què implica
No es pot encadenar No hi ha forma de dir "quan acabi, fes això amb el resultat"
No es pot combinar Per ajuntar dos resultats independents cal fer get() de tots dos
get() bloqueja El fil que orquestra s'atura; amb diversos passos, s'atura diverses vegades
No hi ha callbacks No es pot registrar codi que s'executi en completar-se
No es pot completar a mà No serveix per adaptar API basades en callbacks

El que es voldria escriure és això:

// El mateix amb CompletableFuture: es DECLARA la cadena i es retorna
// immediatament. Cap fil no es bloqueja en cap moment.
CompletableFuture<Path> informe =
        CompletableFuture.supplyAsync(() -> carregarCataleg(), poolEs)
                         .thenApplyAsync(c -> calcularMultes(c), poolCalcul)
                         .thenApplyAsync(total -> exportar(total), poolEs)
                         .exceptionally(error -> camiDError(error));

System.out.println("[main] cadena llancada; el menu continua viu");
mostrarMenu();      // el fil principal NO ha esperat res

Cinc línies que descriuen tot el flux, inclosa la gestió d'errors, sense un sol bloqueig.

  1. Què és un CompletableFuture

CompletableFuture<T> implementa Future<T> —així que continua tenint get(), cancel() i isDone()— i hi afegeix dues capacitats:

  1. És completable: es pot completar manualment des de fora amb complete(valor).
  2. És componible: es poden encadenar etapes que s'executen en completar-se, sense bloquejar.

El <T> és, com sempre, el tipus del resultat: un CompletableFuture<Cataleg> promet un Cataleg; un CompletableFuture<String> promet un String. Els genèrics s'estudien a 10-01.

El model mental correcte és el d'una canonada: declares les etapes per endavant, i cadascuna es dispara quan l'anterior li lliura un valor.

flowchart LR
    A["supplyAsync<br/>carregar catàleg"] -->|"Cataleg"| B["thenApply<br/>calcular multes"]
    B -->|"Double"| C["thenApply<br/>formatar informe"]
    C -->|"String"| D["thenAccept<br/>escriure fitxer"]
    D --> E["Completat"]
    A -.->|"excepció"| F["exceptionally<br/>valor de reserva"]
    B -.->|"excepció"| F
    C -.->|"excepció"| F
    F --> E

Les fletxes contínues són el camí d'èxit: cada etapa rep el resultat de l'anterior. Les discontínues són el camí d'error: una excepció en qualsevol etapa salta directament al gestor, sense executar les etapes intermèdies. És el mateix model que un try/catch que embolcallés tota la seqüència, però repartit en el temps i sense bloquejar.

CompletableFuture implementa a més CompletionStage<T>, la interfície que defineix tots els mètodes de composició. A la pràctica treballaràs amb la classe; la interfície apareix en signatures de mètodes de biblioteques.

  1. Creació: supplyAsync, runAsync, completedFuture

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;

// 1. supplyAsync: executa un Supplier i RETORNA un valor.
//    Es el punt d'entrada mes frequent.
CompletableFuture<Cataleg> f1 =
        CompletableFuture.supplyAsync(() -> carregarCataleg());

// Amb executor propi (recomanat, apartat 4):
CompletableFuture<Cataleg> f2 =
        CompletableFuture.supplyAsync(() -> carregarCataleg(), poolEs);

// 2. runAsync: executa un Runnable, NO retorna valor.
//    El tipus es CompletableFuture<Void>.
CompletableFuture<Void> f3 =
        CompletableFuture.runAsync(() -> registrarAuditoria("arrencada"), poolEs);

// 3. completedFuture: ja completat amb un valor. No executa res.
//    Util per a valors en memoria cau i per a proves.
CompletableFuture<Cataleg> f4 =
        CompletableFuture.completedFuture(catalegEnCache);

// 4. failedFuture (Java 9): ja completat amb una excepcio.
CompletableFuture<Cataleg> f5 =
        CompletableFuture.failedFuture(new BiblioTechException("cataleg no disponible"));

// 5. Buit, per completar-lo manualment despres (apartat 13).
CompletableFuture<Cataleg> f6 = new CompletableFuture<>();

completedFuture és més útil del que sembla: permet que un mètode que de vegades té la resposta immediata i de vegades no retorni sempre el mateix tipus.

/**
 * Retorna SEMPRE un CompletableFuture, tant si la dada es a la
 * memoria cau (resposta immediata) com si cal calcular-la (asincron).
 * Qui crida no ha de distingir els dos casos.
 */
public CompletableFuture<Fitxa> obtenirFitxa(String isbn) {
    Fitxa enCache = cache.get(isbn);
    if (enCache != null) {
        return CompletableFuture.completedFuture(enCache);   // ja llesta
    }
    return CompletableFuture.supplyAsync(() -> construirFitxa(isbn), poolEs);
}

  1. L'executor per defecte i per què convé passar el teu

Si no passes un Executor, supplyAsync i les variants ...Async fan servir ForkJoinPool.commonPool(): el pool comú de la JVM que vas veure a 08-05, amb nuclis - 1 fils.

Això està bé per a tasques curtes i de càlcul, i és un problema seriós per a tota la resta:

// PERILLOS: E/S al pool comu.
// El pool comu te (nuclis - 1) fils i el COMPARTEIX tota la JVM:
// els streams parallels (10-04), altres llibreries i la resta del teu
// codi. Amb 7 fils i 10 lectures de fitxer lentes, el pool queda
// inservible per a tothom.
CompletableFuture.supplyAsync(() -> Files.readAllLines(camiEnorme));

// CORRECTE: executor propi per a E/S, dimensionat segons 08-05.
private static final ExecutorService POOL_ES = new ThreadPoolExecutor(
        16, 16, 0L, TimeUnit.MILLISECONDS,
        new ArrayBlockingQueue<>(200),
        r -> new Thread(r, "bibliotech-async-es-" + comptador.getAndIncrement()),
        new ThreadPoolExecutor.CallerRunsPolicy());

CompletableFuture.supplyAsync(() -> Files.readAllLines(camiEnorme), POOL_ES);

Un detall que sorprèn i que cal conèixer: si la màquina té un sol nucli, commonPool()paral·lelisme 0 i executa les tasques al fil que les envia. El teu codi "asíncron" es torna síncron sense previ avís, i només ho descobreixes en un contenidor amb un nucli assignat.

Executor Fils Quan fer-lo servir
ForkJoinPool.commonPool() (per defecte) nuclis - 1 Càlcul curt i no blocant
Pool propi de càlcul nuclis Càlcul intensiu aïllat
Pool propi d'E/S Molts més Fitxers, esperes: sempre un de propi
newVirtualThreadPerTaskExecutor() (Java 21) Un de virtual per tasca E/S massiva (10-06)

La regla, sense matisos: per a tasques que bloquegen, passa sempre el teu propi Executor. I amb noms de fil decents, que és el consell de 08-02 i 08-05 aplicat aquí.

  1. Transformació: thenApply, thenAccept, thenRun

Tres formes d'encadenar segons què rep i què retorna l'etapa:

Mètode Rep Retorna Resultat
thenApply(Function) El valor anterior Un valor nou CompletableFuture<U>
thenAccept(Consumer) El valor anterior Res CompletableFuture<Void>
thenRun(Runnable) Res Res CompletableFuture<Void>
import java.util.concurrent.CompletableFuture;

CompletableFuture<Void> cadena =
    CompletableFuture
        // 1. Produeix un Cataleg
        .supplyAsync(() -> carregarCataleg(), POOL_ES)

        // 2. thenApply: Cataleg -> Double. Transforma.
        .thenApply(cataleg -> calcularMultesTotals(cataleg))

        // 3. thenApply: Double -> String. Una altra transformacio.
        .thenApply(total -> String.format("Multes pendents: %.2f EUR", total))

        // 4. thenAccept: consumeix el String, no produeix res.
        .thenAccept(text -> System.out.println(text))

        // 5. thenRun: no rep ni retorna. Per a efectes finals.
        .thenRun(() -> LOG.info("Informe de multes completat"));

System.out.println("[main] cadena declarada, continuo treballant");

Aquests són exactament els tipus funcionals de 04-06: Function<T,R>, Consumer<T> i Runnable. Tota l'API de CompletableFuture està construïda sobre ells, així que si domines aquella lliçó, aquesta n'és l'aplicació natural.

Nota important sobre l'ordre: declarar la cadena no l'executa. supplyAsync llança la primera etapa immediatament; les altres queden registrades i es disparen quan els arriba el torn. El main continua sense esperar.

  1. thenApply davant de thenCompose

Aquesta és la distinció més important de la lliçó i la font de confusió número u.

thenApply es fa servir quan la funció retorna un valor normal. thenCompose es fa servir quan la funció retorna un altre CompletableFuture.

// Metode que retorna un valor normal:
Double calcularMultes(Cataleg c) { ... }

// Metode que retorna un CompletableFuture (perque es asincron):
CompletableFuture<Double> calcularMultesAsync(Cataleg c) { ... }

Si fas servir thenApply amb el segon, el tipus s'imbrica:

// L'ERROR: thenApply amb una funcio que retorna CompletableFuture.
CompletableFuture<CompletableFuture<Double>> imbricat =
        CompletableFuture.supplyAsync(() -> carregarCataleg())
                         .thenApply(c -> calcularMultesAsync(c));

// Ara, per arribar al Double, cal desembolcallar DUES vegades:
Double d = imbricat.get().get();     // horrible, i bloqueja dues vegades

// LA SOLUCIO: thenCompose APLANA la imbricacio.
CompletableFuture<Double> pla =
        CompletableFuture.supplyAsync(() -> carregarCataleg())
                         .thenCompose(c -> calcularMultesAsync(c));

Double d2 = pla.join();              // un sol nivell

Regla mnemotècnica:

Si la funció retorna… Fes servir Analogia amb col·leccions
Un valor U thenApply map
Un CompletableFuture<U> thenCompose flatMap

Exemple complet amb les dues, a BiblioTech:

package com.nexussoftware.bibliotech.servei;

import java.util.concurrent.CompletableFuture;

public class ConsultaAsincrona {

    /** Sincron: retorna el valor directament. */
    private Material cercarMaterial(String isbn) { ... }

    /** Asincron: retorna un CompletableFuture. */
    private CompletableFuture<Fitxa> construirFitxaAsync(Material m) { ... }

    /** Sincron: transformacio barata. */
    private String formatar(Fitxa f) { ... }

    /**
     * Cadena que alterna operacions sincrones i asincrones.
     * Fixa't que thenCompose apareix exactament on la
     * funcio retorna un CompletableFuture.
     */
    public CompletableFuture<String> fitxaFormatada(String isbn) {
        return CompletableFuture
                .supplyAsync(() -> cercarMaterial(isbn), POOL_ES)   // -> Material
                .thenCompose(this::construirFitxaAsync)             // -> Fitxa (async)
                .thenApply(this::formatar);                         // -> String (sync)
    }
}

Com detectar l'error al teu propi codi: si veus un tipus CompletableFuture<CompletableFuture<...>> en un missatge del compilador o a l'IDE, has fet servir thenApply on tocava thenCompose. És un error de compilació quan declares el tipus, i passa desapercebut si fas servir var.

  1. Les variants ...Async i en quin fil s'executa cada etapa

Gairebé tots els mètodes tenen tres formes:

.thenApply(f)                  // 1. sense sufix
.thenApplyAsync(f)             // 2. amb sufix, executor per defecte
.thenApplyAsync(f, executor)   // 3. amb sufix i executor explicit

La diferència és en quin fil executa l'etapa:

Forma Fil que executa l'etapa
thenApply(f) El fil que va completar l'etapa anterior, o el fil que crida si ja estava completa
thenApplyAsync(f) Un fil del ForkJoinPool.commonPool()
thenApplyAsync(f, ex) Un fil d'ex

La primera fila amaga una subtilesa important: amb la forma sense sufix, no saps amb certesa en quin fil s'executarà l'etapa. Si l'etapa anterior ja havia acabat quan registres la següent, l'executa el fil que fa el registre —que pot ser main—.

public class QuinFilExecuta {

    public static void main(String[] args) throws Exception {

        ExecutorService pool = Executors.newFixedThreadPool(2,
                r -> new Thread(r, "bibliotech-pool"));

        System.out.println("main executa a: " + Thread.currentThread().getName());

        CompletableFuture<String> f = CompletableFuture
                .supplyAsync(() -> {
                    traca("supplyAsync");
                    return "cataleg";
                }, pool)
                .thenApply(v -> {
                    traca("thenApply");          // el fil del pool
                    return v + "-processat";
                })
                .thenApplyAsync(v -> {
                    traca("thenApplyAsync");     // commonPool
                    return v + "-async";
                })
                .thenApplyAsync(v -> {
                    traca("thenApplyAsync(pool)"); // el pool que li donem
                    return v + "-propi";
                }, pool);

        System.out.println("resultat: " + f.join());
        pool.shutdown();
    }

    static void traca(String etapa) {
        System.out.printf("  %-24s -> %s%n", etapa, Thread.currentThread().getName());
    }
}

Sortida:

main executa a: main
  supplyAsync              -> bibliotech-pool
  thenApply                -> bibliotech-pool
  thenApplyAsync           -> ForkJoinPool.commonPool-worker-1
  thenApplyAsync(pool)     -> bibliotech-pool
resultat: cataleg-processat-async-propi

Com decidir:

  • Transformacions barates (formatar, mapar, sumar): thenApply sense sufix. Evita un canvi de fil innecessari.
  • Feina cara o blocant: thenApplyAsync amb el teu executor. Si no, ocupes el fil que va completar l'etapa anterior —que pot ser un fil del pool comú, o fins i tot main—.
// MALAMENT: bloquejar en una etapa sense sufix ocupa el fil anterior,
// que podria ser un fil del pool comu o el mateix main.
.thenApply(cataleg -> escriureAlDisc(cataleg))       // 500 ms bloquejant

// BE: la feina pesada va a un pool dedicat.
.thenApplyAsync(cataleg -> escriureAlDisc(cataleg), POOL_ES)

  1. Combinació: thenCombine

thenCombine ajunta els resultats de dos futurs independents que s'executen en paral·lel.

package com.nexussoftware.bibliotech.servei;

import java.util.concurrent.CompletableFuture;

public class ResumAsincron {

    /**
     * Les dues consultes son independents i es llancen alhora.
     * El temps total es el de la MES LENTA, no la suma.
     */
    public CompletableFuture<String> resumComplet(String isbn) {

        CompletableFuture<Material> material =
                CompletableFuture.supplyAsync(() -> cataleg.cercar(isbn), POOL_ES);

        CompletableFuture<Integer> prestecs =
                CompletableFuture.supplyAsync(() -> registre.comptarPrestecs(isbn), POOL_ES);

        // thenCombine espera TOTS DOS i aplica la BiFunction (04-06).
        return material.thenCombine(prestecs, (m, n) ->
                String.format("%s (%s) - %d prestecs historics",
                        m.titol(), m.isbn(), n));
    }

    /** Tres o mes: s'encadenen els thenCombine. */
    public CompletableFuture<InformeComplet> informeComplet(String isbn) {

        CompletableFuture<Material> material =
                CompletableFuture.supplyAsync(() -> cataleg.cercar(isbn), POOL_ES);
        CompletableFuture<Integer> prestecs =
                CompletableFuture.supplyAsync(() -> registre.comptarPrestecs(isbn), POOL_ES);
        CompletableFuture<Double> multes =
                CompletableFuture.supplyAsync(() -> calculadora.multesDe(isbn), POOL_ES);

        return material
                .thenCombine(prestecs, ParcialMaterialPrestecs::new)
                .thenCombine(multes, (parcial, m) ->
                        new InformeComplet(parcial.material(), parcial.prestecs(), m));
    }

    private record ParcialMaterialPrestecs(Material material, int prestecs) { }
}

El punt clau: les tres consultes es llancen alhora. Si cadascuna triga 300 ms, el total és ~300 ms, no 900. És la diferència entre concurrència real i una seqüència disfressada.

Els cosins de thenCombine, menys utilitzats:

Mètode Què fa
thenCombine(altre, BiFunction) Espera tots dos i combina els resultats
thenAcceptBoth(altre, BiConsumer) Espera tots dos, consumeix, no retorna res
runAfterBoth(altre, Runnable) Espera tots dos, ignora els valors
applyToEither(altre, Function) El primer que acabi; aplica la funció
acceptEither(altre, Consumer) El primer que acabi; el consumeix
runAfterEither(altre, Runnable) El primer que acabi

  1. allOf i anyOf

Per a N futurs en lloc de dos.

allOf(cf1, cf2, ...) retorna un CompletableFuture<Void> que es completa quan tots acaben. Retorna Void, així que cal recollir els resultats a part.

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

public class AvisosAsincrons {

    /**
     * Envia tots els avisos en parallel i espera que acabin TOTS.
     *
     * El patro de recollida es sempre el mateix:
     *   1. Crear la llista de futurs.
     *   2. allOf(...) sobre l'array.
     *   3. thenApply que recorre els futurs i crida join()
     *      —segur, perque allOf garanteix que ja estan complets—.
     */
    public CompletableFuture<List<ResultatAvis>> enviarTots(List<Prestec> vencuts) {

        List<CompletableFuture<ResultatAvis>> futurs = new ArrayList<>();
        for (Prestec p : vencuts) {
            futurs.add(CompletableFuture.supplyAsync(() -> enviarAvis(p), POOL_ES));
        }

        // allOf rep un array, no una llista.
        CompletableFuture<Void> tots =
                CompletableFuture.allOf(futurs.toArray(new CompletableFuture[0]));

        return tots.thenApply(v -> {
            List<ResultatAvis> resultats = new ArrayList<>(futurs.size());
            for (CompletableFuture<ResultatAvis> f : futurs) {
                // join() aqui NO bloqueja: allOf ja ha garantit que
                // tots estan complets.
                resultats.add(f.join());
            }
            return resultats;
        });
    }

    /**
     * Variant TOLERANT A FALLADES: un avis fallit no tomba el conjunt.
     *
     * COMPTE: si un futur falla, l'allOf tambe falla i el seu join()
     * rellancaria l'excepcio. La solucio es blindar CADA futur amb
     * el seu propi exceptionally ABANS d'agregar-los.
     */
    public CompletableFuture<List<ResultatAvis>> enviarTotsTolerant(
            List<Prestec> vencuts) {

        List<CompletableFuture<ResultatAvis>> futurs = new ArrayList<>();
        for (Prestec p : vencuts) {
            futurs.add(CompletableFuture
                    .supplyAsync(() -> enviarAvis(p), POOL_ES)
                    .exceptionally(e -> ResultatAvis.fallada(p, e.getMessage())));
        }

        return CompletableFuture
                .allOf(futurs.toArray(new CompletableFuture[0]))
                .thenApply(v -> {
                    List<ResultatAvis> r = new ArrayList<>();
                    for (CompletableFuture<ResultatAvis> f : futurs) r.add(f.join());
                    return r;
                });
    }
}

anyOf(cf1, cf2, ...) es completa amb el resultat del primer que acabi, per bé o per mal. Retorna CompletableFuture<Object> —una limitació de l'API, perquè els futurs poden ser de tipus diferents—.

// Tres fonts per al mateix cataleg; ens val la primera.
CompletableFuture<Cataleg> cache   = llegirDeCache();
CompletableFuture<Cataleg> fitxer  = llegirDelFitxerPrincipal();
CompletableFuture<Cataleg> reserva = llegirDeCopiaSeguretat();

CompletableFuture<Object> primer =
        CompletableFuture.anyOf(cache, fitxer, reserva);

Cataleg c = (Cataleg) primer.join();        // cal fer casting

Compte amb anyOf: es completa amb el primer que acabi fins i tot si acaba amb una excepció. Si vols el primer que acabi amb èxit, cal blindar cadascun amb exceptionally o fer servir l'invokeAny de 08-05.

allOf anyOf
Es completa quan Acaben tots Acaba el primer
Tipus del resultat CompletableFuture<Void> CompletableFuture<Object>
Si un falla El conjunt falla Es pot completar amb aquella fallada
Cancel·la els altres No No (continuen executant-se)
Ús Feina en lot Fonts redundants

  1. Errors: exceptionally, handle, whenComplete

Tres mètodes amb papers diferents:

Mètode S'executa Rep Pot canviar el resultat
exceptionally(Function) Només si hi ha error L'excepció : dona un valor de reserva
handle(BiFunction) Sempre Valor i excepció (un serà null)
whenComplete(BiConsumer) Sempre Valor i excepció No: només observa
package com.nexussoftware.bibliotech.servei;

import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;

public class GestioErrorsAsincrona {

    private static final Logger LOG =
            Logger.getLogger(GestioErrorsAsincrona.class.getName());

    /** 1. exceptionally: valor de reserva si alguna cosa falla. */
    public CompletableFuture<Cataleg> carregarAmbReserva() {
        return CompletableFuture
                .supplyAsync(() -> carregarDelFitxer(), POOL_ES)
                .exceptionally(error -> {
                    // 'error' es una CompletionException que EMBOLCALLA la causa.
                    LOG.log(Level.WARNING, "Fallada en carregar; faig servir la reserva",
                            error.getCause());
                    return Cataleg.buit();        // la cadena CONTINUA
                });
    }

    /** 2. handle: s'executa sempre, i unifica els dos camins. */
    public CompletableFuture<String> informeAmbHandle() {
        return CompletableFuture
                .supplyAsync(() -> generarInforme(), POOL_ES)
                .handle((resultat, error) -> {
                    // Exactament un dels dos es null.
                    if (error != null) {
                        LOG.log(Level.SEVERE, "Informe fallit", error);
                        return "INFORME NO DISPONIBLE: " + causaDe(error).getMessage();
                    }
                    return "INFORME OK: " + resultat;
                });
    }

    /** 3. whenComplete: observa sense alterar. Ideal per a traces i metriques. */
    public CompletableFuture<Cataleg> carregarAmbTraca() {
        long inici = System.nanoTime();
        return CompletableFuture
                .supplyAsync(() -> carregarDelFitxer(), POOL_ES)
                .whenComplete((cataleg, error) -> {
                    long ms = (System.nanoTime() - inici) / 1_000_000;
                    if (error != null) {
                        LOG.log(Level.WARNING, "Carrega fallida en " + ms + " ms", error);
                    } else {
                        LOG.log(Level.INFO, "Cataleg carregat en {0} ms ({1} materials)",
                                new Object[] { ms, cataleg.mida() });
                    }
                    // Retornar alguna cosa aqui NO canviaria el resultat:
                    // whenComplete nomes OBSERVA. L'error continua propagant-se.
                });
    }

    /** Utilitat: desembolcallar la causa real. */
    static Throwable causaDe(Throwable t) {
        return (t instanceof java.util.concurrent.CompletionException
                || t instanceof java.util.concurrent.ExecutionException)
                && t.getCause() != null ? t.getCause() : t;
    }
}

El detall de whenComplete que confon: no altera res. Si l'etapa va fallar, el CompletableFuture resultant continua fallant encara que el teu BiConsumer hagi registrat l'error tranquil·lament. És un observador, no un gestor. Per gestionar, handle o exceptionally.

Combinació habitual i recomanable:

CompletableFuture<Path> informe = CompletableFuture
        .supplyAsync(() -> carregarCataleg(), POOL_ES)
        .thenApplyAsync(this::calcularMultes, POOL_CALCUL)
        .thenApplyAsync(this::exportar, POOL_ES)
        .whenComplete((cami, error) -> registrarMetrica(cami, error))   // observar
        .exceptionally(error -> {                                       // gestionar
            LOG.log(Level.SEVERE, "Cadena d'informe fallida", error);
            return Path.of("informes/error.txt");
        });

  1. Com viatja una excepció per la cadena

Quan una etapa llança una excepció, totes les etapes següents se salten fins a trobar un gestor. És igual que un throw que travessa diversos mètodes.

public class PropagacioErrors {

    public static void main(String[] args) {

        CompletableFuture<String> cadena = CompletableFuture
                .supplyAsync(() -> {
                    System.out.println("  etapa 1: OK");
                    return "cataleg";
                })
                .thenApply(v -> {
                    System.out.println("  etapa 2: llancant excepcio");
                    throw new IllegalStateException("cataleg corrupte");
                })
                .thenApply(v -> {
                    System.out.println("  etapa 3: NO S'EXECUTA");
                    return v + "-processat";
                })
                .thenApply(v -> {
                    System.out.println("  etapa 4: TAMPOC");
                    return v.toUpperCase();
                })
                .exceptionally(error -> {
                    System.out.println("  gestor: capturat " + error.getClass().getSimpleName());
                    System.out.println("          causa real: "
                            + error.getCause().getClass().getSimpleName()
                            + ": " + error.getCause().getMessage());
                    return "VALOR-DE-RESERVA";
                })
                .thenApply(v -> {
                    System.out.println("  etapa 5: SI que s'executa, amb la reserva");
                    return v.toLowerCase();
                });

        System.out.println("resultat: " + cadena.join());
    }
}

Sortida:

  etapa 1: OK
  etapa 2: llancant excepcio
  gestor: capturat CompletionException
          causa real: IllegalStateException: cataleg corrupte
  etapa 5: SI que s'executa, amb la reserva
resultat: valor-de-reserva

Tres coses que ensenya aquesta sortida:

1. Les etapes 3 i 4 se salten completament. L'excepció curtcircuita la cadena fins al primer gestor.

2. L'excepció arriba embolcallada en CompletionException. No és la teva IllegalStateException directament: és a getCause(). És el mateix que feia ExecutionException amb Future a 08-05, i per la mateixa raó: l'excepció original és comprovada o no, i cal transportar-la per una API que no la declara.

3. Després d'exceptionally, la cadena continua amb normalitat. L'etapa 5 s'executa amb el valor de reserva. Aquest és exactament el punt d'exceptionally: recuperar i continuar.

L'error clàssic: encadenar després d'exceptionally sense adonar-se'n.

// PARANY: sembla que l'exceptionally protegeix tota la cadena.
CompletableFuture<Path> f = CompletableFuture
        .supplyAsync(() -> carregarCataleg())
        .exceptionally(e -> Cataleg.buit())         // protegeix el de DALT
        .thenApply(c -> calcularMultes(c))           // <-- SENSE protegir
        .thenApply(m -> exportar(m));                // <-- SENSE protegir

// Si calcularMultes o exportar fallen, l'excepcio surt per join()
// i ningu no la gestiona. L'exceptionally ha quedat massa amunt.

// CORRECTE: el gestor al FINAL de la cadena.
CompletableFuture<Path> g = CompletableFuture
        .supplyAsync(() -> carregarCataleg())
        .thenApply(c -> calcularMultes(c))
        .thenApply(m -> exportar(m))
        .exceptionally(e -> {                        // protegeix TOT l'anterior
            LOG.log(Level.SEVERE, "Cadena fallida", e);
            return Path.of("informes/error.txt");
        });

Un gestor només cobreix el que hi ha per damunt d'ell. Si necessites recuperació intermèdia i protecció final, posa'n dos.

L'error més greu de tots: no gestionar res.

// Si aquesta cadena falla, NO PASSA RES VISIBLE. No hi ha traca,
// no hi ha log, no hi ha excepcio. La fallada simplement desapareix.
CompletableFuture.supplyAsync(() -> carregarCataleg())
                 .thenAccept(c -> catalegGlobal = c);

És el mateix problema que submit sense get() a 08-05, i amb les mateixes conseqüències. Tota cadena ha d'acabar en exceptionally, handle o whenComplete.

  1. Temps límit: orTimeout i completeOnTimeout

Java 9 va afegir dos mètodes que eviten haver de muntar un temporitzador a mà.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

// orTimeout: si no es completa dins del termini, FALLA amb TimeoutException.
CompletableFuture<Cataleg> ambTermini = CompletableFuture
        .supplyAsync(() -> carregarCataleg(), POOL_ES)
        .orTimeout(5, TimeUnit.SECONDS)
        .exceptionally(error -> {
            if (causaDe(error) instanceof java.util.concurrent.TimeoutException) {
                LOG.warning("La carrega ha excedit els 5 segons");
            }
            return Cataleg.buit();
        });

// completeOnTimeout: si no es completa dins del termini, es completa
// amb el VALOR PER DEFECTE. No falla.
CompletableFuture<Cataleg> ambDefecte = CompletableFuture
        .supplyAsync(() -> carregarCataleg(), POOL_ES)
        .completeOnTimeout(Cataleg.buit(), 5, TimeUnit.SECONDS);
orTimeout(t, u) completeOnTimeout(v, t, u)
En exhaurir-se el termini Falla amb TimeoutException Es completa amb v
Cal gestionar-ho? No
Cancel·la la tasca subjacent? No No
Fer servir quan Vols assabentar-te del retard Tens un valor degradat acceptable

L'avís imprescindible: cap dels dos no cancel·la la tasca que continua corrent. El CompletableFuture es completa —amb error o amb el valor per defecte—, però el fil que estava carregant el catàleg continua treballant i consumint recursos fins que acabi sol. És el mateix que la TimeoutException de Future.get() a 08-05.

  1. Completar manualment: adaptar una API de callbacks

Aquí es cobra la "C" de CompletableFuture. Un CompletableFuture buit es pot completar des de qualsevol fil, cosa que permet embolcallar una API antiga basada en callbacks:

package com.nexussoftware.bibliotech.persistencia;

import java.util.concurrent.CompletableFuture;

public class AdaptadorCallbacks {

    /**
     * API antiga basada en callbacks. Es incomoda de compondre:
     * imbricar-ne tres produeix la "piramide de la mort".
     */
    interface LectorAmbCallback {
        void llegirAsync(String isbn, Callback<Material> callback);
    }

    interface Callback<T> {
        void alCompletar(T resultat);
        void alFallar(Throwable error);
    }

    private final LectorAmbCallback lector;

    public AdaptadorCallbacks(LectorAmbCallback lector) { this.lector = lector; }

    /**
     * Converteix l'API de callbacks en un CompletableFuture componible.
     * Es el patro estandard per modernitzar codi heretat sense tocar-lo.
     */
    public CompletableFuture<Material> llegir(String isbn) {

        // 1. Futur buit, sense tasca associada.
        CompletableFuture<Material> futur = new CompletableFuture<>();

        // 2. Es llanca l'operacio amb un callback que el completa.
        lector.llegirAsync(isbn, new Callback<Material>() {
            @Override
            public void alCompletar(Material m) {
                futur.complete(m);                     // exit
            }
            @Override
            public void alFallar(Throwable error) {
                futur.completeExceptionally(error);    // fallada
            }
        });

        // 3. Es retorna immediatament, sense esperar el callback.
        return futur;
    }

    /**
     * I ara l'API antiga es componible com qualsevol altra:
     * tres lectures en parallel i combinacio, sense imbricacio.
     */
    public CompletableFuture<String> compararTres(String i1, String i2, String i3) {
        return llegir(i1)
                .thenCombine(llegir(i2), (a, b) -> a.titol() + " / " + b.titol())
                .thenCombine(llegir(i3), (parell, c) -> parell + " / " + c.titol());
    }
}

Mètodes de completat manual:

Mètode Què fa
complete(v) Completa amb v; retorna false si ja estava complet
completeExceptionally(t) Completa amb l'excepció t
completeAsync(supplier, ex) Completa executant el Supplier a ex (Java 9)
getNow(valorSiNoEstaLlest) Retorna el valor sense bloquejar, o el defecte
isCompletedExceptionally() Ha acabat amb error?

  1. Cancel·lació i els seus límits

CompletableFuture.cancel(boolean) existeix perquè implementa Future, però funciona diferent del que esperes:

CompletableFuture<Cataleg> f =
        CompletableFuture.supplyAsync(() -> carregarCatalegLent(), POOL_ES);

TimeUnit.SECONDS.sleep(1);
boolean cancellat = f.cancel(true);       // el 'true' s'IGNORA

System.out.println("cancel(): " + cancellat);             // true
System.out.println("isCancelled(): " + f.isCancelled());  // true
// PERO: el fil de POOL_ES continua executant carregarCatalegLent()
// fins al final. El parametre mayInterruptIfRunning NO FA RES.

El que fa cancel: completa el CompletableFuture amb una CancellationException, de manera que les etapes següents no s'executen i join() llança.

El que NO fa: interrompre el fil que executa la tasca. El paràmetre mayInterruptIfRunning s'ignora, i així ho diu la documentació. La tasca continua fins a acabar sola.

Si necessites cancel·lació real, cal implementar-la amb el protocol de 08-02:

package com.nexussoftware.bibliotech.persistencia;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Cancellacio REAL d'una tasca asincrona: un marcador compartit
 * que la tasca consulta. Es la interrupcio cooperativa de 08-02
 * adaptada a CompletableFuture, que no pot interrompre per si sol.
 */
public class ImportacioCancellable {

    private final AtomicBoolean cancellada = new AtomicBoolean(false);
    private final CompletableFuture<Informe> futur;

    public ImportacioCancellable(Path fitxer) {
        this.futur = CompletableFuture.supplyAsync(() -> importar(fitxer), POOL_ES);
    }

    private Informe importar(Path fitxer) {
        int processades = 0;
        for (String linia : llegirLinies(fitxer)) {

            // PUNT DE CANCELLACIO explicit.
            if (cancellada.get() || Thread.currentThread().isInterrupted()) {
                throw new CancellationException(
                        "Importacio cancellada despres de " + processades + " linies");
            }
            processar(linia);
            processades++;
        }
        return new Informe(processades);
    }

    /** Cancellacio que SI atura la feina. */
    public void cancellar() {
        cancellada.set(true);         // la tasca ho veura i avortara
        futur.cancel(false);          // i el futur es completa ja
    }

    public CompletableFuture<Informe> futur() { return futur; }
}

  1. Bones pràctiques i paranys

Parany 1: bloquejar dins d'una etapa.

// PESSIM: join() dins d'una etapa bloqueja un fil del pool.
// Amb prou cadenes aixi, el pool s'exhaureix i tot s'atura.
.thenApply(cataleg -> {
    Double multes = calcularMultesAsync(cataleg).join();    // BLOQUEJA
    return multes;
})

// CORRECTE: thenCompose aplana sense bloquejar ningu.
.thenCompose(cataleg -> calcularMultesAsync(cataleg))

Parany 2: fer servir el pool comú per a E/S. Ja vist a l'apartat 4: nuclis - 1 fils compartits per tota la JVM. Passa sempre el teu executor per a feina blocant.

Parany 3: join() davant de get(). Tots dos bloquegen; la diferència és a les excepcions:

// get(): llanca ExecutionException i InterruptedException, totes dues COMPROVADES.
try {
    Cataleg c = futur.get();
} catch (ExecutionException | InterruptedException e) { ... }

// join(): llanca CompletionException, NO comprovada. Mes comode
// dins de lambdes, on una excepcio comprovada no compila.
Cataleg c = futur.join();

Dins d'una lambda, join() és gairebé obligatori perquè get() no compilaria. Fora, get(timeout) és preferible pel termini.

Parany 4: no saber en quin fil s'executa cada etapa. Repassa l'apartat 7. Una etapa sense sufix Async pot acabar executant-se a main.

Parany 5: cadenes il·legibles. Deu etapes encadenades són tan dolentes de llegir com deu if imbricats. Extreu mètodes:

// MALAMENT: una sola expressio de vint linies.

// BE: cada pas amb nom.
public CompletableFuture<Path> generarInformeMensual() {
    return carregarCataleg()
            .thenCompose(this::enriquirAmbPrestecs)
            .thenApplyAsync(this::calcularMultes, POOL_CALCUL)
            .thenApplyAsync(this::formatar, POOL_CALCUL)
            .thenApplyAsync(this::escriureFitxer, POOL_ES)
            .orTimeout(60, TimeUnit.SECONDS)
            .whenComplete(this::registrarMetriques)
            .exceptionally(this::informeDError);
}

Parany 6: oblidar el gestor final. Una fallada sense gestionar desapareix sense deixar rastre.

Les sis regles, resumides:

  1. Executor propi per a tot el que bloquegi.
  2. No bloquejar mai dins d'una etapa: thenCompose, no join().
  3. thenCompose quan la funció retorna un futur; thenApply quan retorna un valor.
  4. Gestor final sempre: exceptionally o handle al final de la cadena.
  5. ...Async amb el teu executor per a la feina cara; sense sufix per a transformacions barates.
  6. Extreu mètodes amb nom: una cadena s'ha de llegir com una llista de passos.

  1. BiblioTech: la cadena asíncrona completa

El tancament del mòdul. Una operació de negoci real —generar l'informe mensual de multes i exportar-lo— sense bloquejar el menú en cap moment.

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.*;

import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Generacio asincrona de l'informe mensual de BiblioTech.
 *
 * La cadena completa:
 *   1. Carregar el cataleg (E/S)          -- en parallel amb 2
 *   2. Carregar els prestecs actius (E/S) -- en parallel amb 1
 *   3. Combinar tots dos
 *   4. Calcular les multes (CPU)
 *   5. Formatar l'informe (CPU)
 *   6. Escriure el fitxer (E/S)
 *
 * CAP fil no es bloqueja esperant: el menu continua atenent l'usuari
 * durant tota l'operacio.
 */
public class GeneradorInformeAsincron implements AutoCloseable {

    private static final Logger LOG =
            Logger.getLogger(GeneradorInformeAsincron.class.getName());

    /** Pool d'E/S: molts fils perque gairebe tot es espera (08-01). */
    private final ExecutorService poolEs;
    /** Pool de calcul: un fil per nucli. */
    private final ExecutorService poolCalcul;

    private final CatalegConcurrent cataleg;
    private final RegistrePrestecsSegur registre;
    private final CalculadoraMultes calculadora;

    public GeneradorInformeAsincron(CatalegConcurrent cataleg,
                                    RegistrePrestecsSegur registre,
                                    CalculadoraMultes calculadora) {
        this.cataleg = cataleg;
        this.registre = registre;
        this.calculadora = calculadora;

        AtomicInteger nEs = new AtomicInteger(1);
        this.poolEs = new ThreadPoolExecutor(
                16, 16, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(200),
                r -> new Thread(r, "bibliotech-async-es-" + nEs.getAndIncrement()),
                new ThreadPoolExecutor.CallerRunsPolicy());

        AtomicInteger nCpu = new AtomicInteger(1);
        this.poolCalcul = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors(),
                r -> new Thread(r, "bibliotech-async-cpu-" + nCpu.getAndIncrement()));
    }

    // ---------- ETAPES ----------

    private CompletableFuture<List<Material>> carregarMaterials() {
        return CompletableFuture.supplyAsync(() -> {
            traca("carregant materials");
            dormir(400);                       // E/S simulada
            return cataleg.perTipus(TipusMaterial.LLIBRE);
        }, poolEs);
    }

    private CompletableFuture<List<Prestec>> carregarPrestecsActius() {
        return CompletableFuture.supplyAsync(() -> {
            traca("carregant prestecs");
            dormir(500);                       // E/S simulada, EN PARALLEL
            return registre.totsElsActius();
        }, poolEs);
    }

    /** Etapa de CPU: es mana al pool de calcul explicitament. */
    private DadesInforme calcular(List<Material> materials, List<Prestec> prestecs) {
        traca("calculant multes");
        double total = 0;
        int vencuts = 0;
        for (Prestec p : prestecs) {
            double m = calculadora.calcular(p);
            if (m > 0) { total += m; vencuts++; }
        }
        return new DadesInforme(materials.size(), prestecs.size(), vencuts, total);
    }

    private String formatar(DadesInforme d) {
        traca("formatant");
        return """
               ===========================================
                 BiblioTech - Informe mensual de multes
                 Nexus Software
               ===========================================
                 Materials al cataleg  : %d
                 Prestecs actius       : %d
                 Prestecs vencuts      : %d
                 Multes acumulades     : %.2f EUR
               ===========================================
               """.formatted(d.materials(), d.prestecs(), d.vencuts(), d.multes());
    }

    private Path escriure(String contingut) {
        traca("escrivint fitxer");
        dormir(300);                           // E/S simulada
        Path desti = Path.of("informes", "multes-mensual.txt");
        // Al codi real: EscripturaAtomica.escriure(desti, contingut) de 07-06
        return desti;
    }

    // ---------- LA CADENA ----------

    /**
     * Declara tota la cadena i RETORNA IMMEDIATAMENT.
     * El fil que crida no espera res.
     */
    public CompletableFuture<Path> generar() {

        long inici = System.nanoTime();

        // 1 i 2 arrenquen ALHORA: no s'encadenen, es combinen.
        CompletableFuture<List<Material>> materials = carregarMaterials();
        CompletableFuture<List<Prestec>> prestecs = carregarPrestecsActius();

        return materials
                // 3. Esperar tots dos i combinar-los. La feina es mana al
                //    pool de calcul amb thenCombineAsync, per no ocupar
                //    un fil d'E/S amb feina de CPU.
                .thenCombineAsync(prestecs, this::calcular, poolCalcul)

                // 4. Formatar: CPU, mateix pool.
                .thenApplyAsync(this::formatar, poolCalcul)

                // 5. Escriure: E/S, pool d'E/S.
                .thenApplyAsync(this::escriure, poolEs)

                // 6. Termini global.
                .orTimeout(30, TimeUnit.SECONDS)

                // 7. Observar sense alterar: metriques.
                .whenComplete((cami, error) -> {
                    long ms = (System.nanoTime() - inici) / 1_000_000;
                    if (error == null) {
                        LOG.log(Level.INFO, "Informe generat en {0} ms: {1}",
                                new Object[] { ms, cami });
                    } else {
                        LOG.log(Level.SEVERE, "Informe fallit despres de " + ms + " ms", error);
                    }
                })

                // 8. Gestionar: al FINAL, per cobrir tota la cadena.
                .exceptionally(error -> {
                    Throwable causa = causaDe(error);
                    if (causa instanceof TimeoutException) {
                        LOG.warning("L'informe ha excedit els 30 segons");
                    }
                    return Path.of("informes", "informe-no-disponible.txt");
                });
    }

    // ---------- AUXILIARS ----------

    record DadesInforme(int materials, int prestecs, int vencuts, double multes) { }

    static Throwable causaDe(Throwable t) {
        return (t instanceof CompletionException || t instanceof ExecutionException)
                && t.getCause() != null ? t.getCause() : t;
    }

    private static void traca(String etapa) {
        System.out.printf("      [%s] %s%n", Thread.currentThread().getName(), etapa);
    }

    private static void dormir(long ms) {
        try { TimeUnit.MILLISECONDS.sleep(ms); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }

    @Override
    public void close() {
        for (ExecutorService ex : List.of(poolCalcul, poolEs)) {
            ex.shutdown();
            try {
                if (!ex.awaitTermination(15, TimeUnit.SECONDS)) ex.shutdownNow();
            } catch (InterruptedException e) {
                ex.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }
}

El menú que continua viu:

package com.nexussoftware.bibliotech.presentacio;

import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class MenuAmbInformeAsincron {

    public static void main(String[] args) throws Exception {

        try (GeneradorInformeAsincron generador = new GeneradorInformeAsincron(
                cataleg, registre, calculadora)) {

            System.out.println("=== BiblioTech - Nexus Software ===");
            System.out.println("Llancant informe mensual en segon pla...\n");

            // La cadena sencera es declara i retorna a l'instant.
            CompletableFuture<Path> informe = generador.generar();

            // Registrem que fer en acabar. NO esperem.
            informe.thenAccept(cami ->
                    System.out.println("\n>>> Informe llest a: " + cami));

            // El menu continua atenent l'usuari.
            for (int i = 1; i <= 8; i++) {
                System.out.printf("  [menu] opcio %d atesa (el menu NO esta bloquejat)%n", i);
                TimeUnit.MILLISECONDS.sleep(200);
            }

            // Nomes al final, si de veritat cal el resultat, s'espera.
            Path cami = informe.get(30, TimeUnit.SECONDS);
            System.out.println("\n[main] confirmat: " + cami);
        }
    }
}

Sortida:

=== BiblioTech - Nexus Software ===
Llancant informe mensual en segon pla...

  [menu] opcio 1 atesa (el menu NO esta bloquejat)
      [bibliotech-async-es-1] carregant materials
      [bibliotech-async-es-2] carregant prestecs
  [menu] opcio 2 atesa (el menu NO esta bloquejat)
  [menu] opcio 3 atesa (el menu NO esta bloquejat)
      [bibliotech-async-cpu-1] calculant multes
      [bibliotech-async-cpu-1] formatant
  [menu] opcio 4 atesa (el menu NO esta bloquejat)
      [bibliotech-async-es-3] escrivint fitxer

>>> Informe llest a: informes/multes-mensual.txt
  [menu] opcio 5 atesa (el menu NO esta bloquejat)
  [menu] opcio 6 atesa (el menu NO esta bloquejat)
  [menu] opcio 7 atesa (el menu NO esta bloquejat)
  [menu] opcio 8 atesa (el menu NO esta bloquejat)
INFO: Informe generat en 812 ms: informes/multes-mensual.txt

[main] confirmat: informes/multes-mensual.txt

Quatre coses que demostra aquesta sortida:

  1. El menú no s'atura mai. Les vuit opcions s'atenen mentre l'informe es genera. És el cas A de 08-01, resolt de la forma definitiva.
  2. Les dues càrregues ocorren en paral·lel, a async-es-1 i async-es-2. El temps total d'aquesta fase és el de la més lenta (500 ms), no la suma (900 ms).
  3. Cada etapa s'executa al pool correcte: E/S als fils es, càlcul als cpu. L'aïllament per mampares de 08-05, aplicat dins d'una sola cadena.
  4. El total són 812 ms, davant dels 1200 ms de la versió seqüencial (400 + 500 + 300). I —el que és important— durant aquests 812 ms el fil principal no va estar bloquejat ni un instant.

  1. Comparació amb el model reactiu i els fils virtuals

CompletableFuture no és l'última paraula en asincronia en Java. Convé situar-lo.

El model reactiu (Reactive Streams, amb implementacions com Project Reactor i RxJava) generalitza la idea a fluxos de molts valors en lloc d'un sol resultat. Hi afegeix dues coses que CompletableFuture no té: operadors per transformar fluxos complets, i contrapressió, el mecanisme pel qual un consumidor lent li diu al productor que redueixi el ritme —la mateixa idea de les cues acotades de 08-06, integrada al model—. Java 9 va incorporar les interfícies Flow.Publisher i Flow.Subscriber a la biblioteca estàndard, però sense implementació: és un punt de trobada entre llibreries. Spring WebFlux, a 11-02, s'apuntala en aquest model.

Els fils virtuals (Java 21) ataquen el problema des del costat oposat. En lloc de fer el codi asíncron més componible, fan que el codi blocant deixi de ser car:

// Amb CompletableFuture: asincron, componible, i de lectura exigent.
CompletableFuture<Path> f = CompletableFuture
        .supplyAsync(() -> carregarCataleg(), poolEs)
        .thenApplyAsync(this::calcularMultes, poolCpu)
        .thenApplyAsync(this::exportar, poolEs)
        .exceptionally(this::reserva);

// Amb fils virtuals: codi SEQUENCIAL normal, amb try/catch normal,
// que bloqueja un fil virtual —el cost del qual es de nanosegons—.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        Cataleg c = carregarCataleg();      // "bloqueja", pero es baratissim
        double m = calcularMultes(c);
        return exportar(m);
    });
}

El segon es llegeix com a codi seqüencial —amb try/catch normal, traces de pila llegibles i depuració convencional— i escala com el primer, perquè bloquejar un fil virtual no bloqueja cap fil del sistema operatiu. És un canvi de fons en com s'escriurà la concurrència en Java, i s'explica a 10-06.

CompletableFuture Reactiu Fils virtuals
Valors Un Molts (flux) Un
Estil Composició d'etapes Operadors sobre fluxos Seqüencial normal
Contrapressió No Natural (bloqueig real)
Llegibilitat Mitjana Baixa al principi Alta
Depuració Difícil (traces partides) Molt difícil Normal
Des de Java 8 Llibreria / Java 9 (Flow) Java 21
Es veu a Aquesta lliçó 11-02 (WebFlux) 10-06

Quan continua sent CompletableFuture l'eina correcta: quan necessites combinar uns pocs resultats independents, quan treballes amb API que ja el retornen —HttpClient.sendAsync a 09-06, bona part de Spring—, o quan el teu Java és anterior al 21. És una peça per conèixer, no una per aplicar a tot arreu.

Errors Comuns i Consells

Error 1: fer servir thenApply quan la funció retorna un CompletableFuture. El resultat és CompletableFuture<CompletableFuture<T>> i cal desembolcallar dues vegades. Fes servir thenCompose.

Error 2: bloquejar amb join() o get() dins d'una etapa. Ocupa un fil del pool esperant. Amb prou cadenes, el pool s'exhaureix i tot s'atura. thenCompose al seu lloc.

Error 3: fer servir el pool comú per a tasques blocants. nuclis - 1 fils compartits per tota la JVM, inclosos els streams paral·lels. Passa el teu executor.

Error 4: no posar cap gestor d'errors. La fallada desapareix sense traça, sense log i sense excepció. Tota cadena acaba en exceptionally o handle.

Error 5: posar l'exceptionally massa amunt. Només cobreix les etapes anteriors a ell. Per protegir tota la cadena, va al final.

Error 6: oblidar que l'excepció ve embolcallada en CompletionException. L'original és a getCause(). Escriu una utilitat causaDe(Throwable) i fes-la servir sempre.

Error 7: creure que cancel(true) interromp la tasca. El paràmetre s'ignora. Per a cancel·lació real, un marcador compartit i punts de comprovació (08-02).

Error 8: creure que orTimeout cancel·la la feina subjacent. No ho fa: el futur falla, la tasca continua.

Error 9: fer servir allOf amb futurs que poden fallar sense blindar-los. Una fallada fa fallar el conjunt. Posa un exceptionally a cadascun abans d'agregar-los.

Error 10: anyOf esperant el primer èxit. Es completa amb el primer que acabi, encara que acabi amb excepció.

Error 11: cadenes de quinze etapes en una sola expressió. Il·legibles i impossibles de depurar. Extreu mètodes amb nom.

Consell 1: un executor propi, anomenat, per tipus de feina. Un d'E/S i un de càlcul, com a l'apartat 16. Els noms de fil s'agraeixen al primer bolcat (08-03).

Consell 2: whenComplete per a mètriques, exceptionally per recuperar. El primer observa sense alterar; el segon canvia el resultat. Encadena'ls en aquest ordre.

Consell 3: join() dins de lambdes, get(timeout) fora. join llança una excepció no comprovada, que és l'única cosa que compila dins d'una Function.

Consell 4: una cadena s'ha de llegir com una llista de passos. Si no la pots explicar en veu alta llegint-la de dalt a baix, extreu mètodes.

Consell 5: completedFuture unifica els camins ràpid i lent. Un mètode que de vegades respon des de la memòria cau i de vegades calcula ha de retornar sempre el mateix tipus.

Consell 6: si el teu Java és 21+, planteja't si necessites això. Per a una cadena seqüencial de passos blocants, un fil virtual dona el mateix rendiment amb codi molt més simple (10-06). CompletableFuture continua sent millor per combinar resultats independents.

Exercicis

Exercici 1: De Future a CompletableFuture

Pren aquesta operació escrita amb Future i reescriu-la amb CompletableFuture sense cap bloqueig intermedi:

Future<Cataleg> f1 = executor.submit(() -> carregarCataleg());
Cataleg c = f1.get();
Future<List<Prestec>> f2 = executor.submit(() -> carregarPrestecs(c));
List<Prestec> p = f2.get();
Future<Double> f3 = executor.submit(() -> calcularMultes(p));
double total = f3.get();
Future<Path> f4 = executor.submit(() -> exportar(total));
Path informe = f4.get();

La versió nova ha de fer servir un pool d'E/S i un altre de càlcul, aplicar thenCompose on correspongui (fes que carregarPrestecs retorni un CompletableFuture), posar un termini global de 20 segons, registrar el temps total amb whenComplete i gestionar l'error al final retornant un camí de reserva. Mesura i compara el temps de les dues versions simulant 300 ms per operació.

Exercici 2: Fitxa completa combinant tres fonts

Escriu ServeiFitxes amb un mètode CompletableFuture<FitxaCompleta> fitxaCompleta(String isbn) que combini tres consultes independents llançades en paral·lel: les dades del material (400 ms), el nombre de préstecs històrics (600 ms) i la valoració mitjana (300 ms). Requisits:

  1. Les tres es llancen alhora; el temps total ha de ser ~600 ms, no 1300.
  2. Cada consulta individual ha de tenir el seu propi exceptionally amb un valor degradat, de manera que la fallada d'una no impedeixi construir la fitxa.
  3. La combinació es fa amb thenCombine encadenats.
  4. completeOnTimeout amb una fitxa mínima si el conjunt triga més de 2 segons.
  5. Un main que demostri el cas correcte i el cas en què la consulta de valoracions falla.

Exercici 3: Enviament massiu asíncron amb recollida de resultats

Reescriu l'enviament dels 200 avisos de 08-05 amb CompletableFuture. EnviamentAvisosAsincron ha de:

  1. Crear un CompletableFuture<ResultatAvis> per avís, amb un pool d'E/S propi de 16 fils i noms decents.
  2. Blindar cada futur amb exceptionally perquè una fallada individual produeixi un ResultatAvis de fallada en lloc de trencar el conjunt.
  3. Agregar amb allOf i recollir tots els resultats amb el patró de l'apartat 9.
  4. Sobre el futur agregat, encadenar un thenApply que produeixi un ResumEnviament (total, enviats, fallits, mil·lisegons).
  5. Aplicar orTimeout(60, SECONDS) i un exceptionally final.
  6. Mentre la cadena corre, el main ha d'imprimir progrés fent servir un LongAdder que cada tasca incrementi en acabar — demostrant que el fil principal no està bloquejat.

Solucions

Solució a l'Exercici 1

package com.nexussoftware.bibliotech.servei;

import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class CadenaInforme {

    private final ExecutorService poolEs;
    private final ExecutorService poolCpu;

    public CadenaInforme() {
        AtomicInteger a = new AtomicInteger(1);
        this.poolEs = Executors.newFixedThreadPool(8,
                r -> new Thread(r, "informe-es-" + a.getAndIncrement()));
        AtomicInteger b = new AtomicInteger(1);
        this.poolCpu = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors(),
                r -> new Thread(r, "informe-cpu-" + b.getAndIncrement()));
    }

    // --- Operacions simulades ---

    private Cataleg carregarCataleg() {
        dormir(300);
        return new Cataleg();
    }

    /**
     * ASINCRONA: retorna un CompletableFuture, no un valor.
     * Per aixo s'encadena amb thenCompose i no amb thenApply.
     */
    private CompletableFuture<List<Prestec>> carregarPrestecsAsync(Cataleg c) {
        return CompletableFuture.supplyAsync(() -> {
            dormir(300);
            return List.<Prestec>of();
        }, poolEs);
    }

    private double calcularMultes(List<Prestec> p) {
        dormir(300);
        return 137.50;
    }

    private Path exportar(double total) {
        dormir(300);
        return Path.of("informes/multes.txt");
    }

    // --- VERSIO BLOCANT (l'original) ---

    public Path versioAmbFuture() throws Exception {
        ExecutorService ex = Executors.newFixedThreadPool(4);
        try {
            Future<Cataleg> f1 = ex.submit(this::carregarCataleg);
            Cataleg c = f1.get();                                  // BLOQUEJA

            Future<List<Prestec>> f2 = ex.submit(() -> {
                dormir(300); return List.<Prestec>of();
            });
            List<Prestec> p = f2.get();                            // BLOQUEJA

            Future<Double> f3 = ex.submit(() -> calcularMultes(p));
            double total = f3.get();                               // BLOQUEJA

            Future<Path> f4 = ex.submit(() -> exportar(total));
            return f4.get();                                       // BLOQUEJA
        } finally {
            ex.shutdown();
        }
    }

    // --- VERSIO ASINCRONA ---

    public CompletableFuture<Path> versioAmbCompletableFuture() {

        long inici = System.nanoTime();

        return CompletableFuture
                // 1. E/S: pool d'E/S
                .supplyAsync(this::carregarCataleg, poolEs)

                // 2. thenCompose perque carregarPrestecsAsync retorna
                //    un CompletableFuture. Amb thenApply obtindriem
                //    CompletableFuture<CompletableFuture<List<Prestec>>>.
                .thenCompose(this::carregarPrestecsAsync)

                // 3. CPU: pool de calcul
                .thenApplyAsync(this::calcularMultes, poolCpu)

                // 4. E/S: pool d'E/S
                .thenApplyAsync(this::exportar, poolEs)

                // 5. Termini global
                .orTimeout(20, TimeUnit.SECONDS)

                // 6. Observar sense alterar
                .whenComplete((cami, error) -> {
                    long ms = (System.nanoTime() - inici) / 1_000_000;
                    System.out.printf("  [cadena] acabada en %d ms (%s)%n",
                            ms, error == null ? "OK" : "ERROR");
                })

                // 7. Gestionar al FINAL: cobreix totes les etapes anteriors
                .exceptionally(error -> {
                    System.err.println("  [cadena] fallada: " + causaDe(error).getMessage());
                    return Path.of("informes/no-disponible.txt");
                });
    }

    static Throwable causaDe(Throwable t) {
        return (t instanceof CompletionException || t instanceof ExecutionException)
                && t.getCause() != null ? t.getCause() : t;
    }

    static void dormir(long ms) {
        try { TimeUnit.MILLISECONDS.sleep(ms); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }

    public void tancar() {
        poolEs.shutdown();
        poolCpu.shutdown();
    }

    public static void main(String[] args) throws Exception {
        CadenaInforme ci = new CadenaInforme();
        try {
            System.out.println("=== VERSIO AMB FUTURE (blocant) ===");
            long t1 = System.nanoTime();
            Path p1 = ci.versioAmbFuture();
            long ms1 = (System.nanoTime() - t1) / 1_000_000;
            System.out.println("  resultat: " + p1 + "  (" + ms1 + " ms)");
            System.out.println("  el fil main ha estat bloquejat 4 vegades\n");

            System.out.println("=== VERSIO AMB COMPLETABLEFUTURE ===");
            long t2 = System.nanoTime();
            CompletableFuture<Path> f = ci.versioAmbCompletableFuture();
            long msDeclaracio = (System.nanoTime() - t2) / 1_000_000;
            System.out.println("  cadena declarada en " + msDeclaracio + " ms");
            System.out.println("  el fil main continua lliure; faig altres coses...");

            for (int i = 1; i <= 5; i++) {
                System.out.println("    feina del main " + i);
                TimeUnit.MILLISECONDS.sleep(150);
            }

            Path p2 = f.get(20, TimeUnit.SECONDS);
            long ms2 = (System.nanoTime() - t2) / 1_000_000;
            System.out.println("  resultat: " + p2 + "  (" + ms2 + " ms)");
        } finally {
            ci.tancar();
        }
    }
}

Sortida:

=== VERSIO AMB FUTURE (blocant) ===
  resultat: informes/multes.txt  (1214 ms)
  el fil main ha estat bloquejat 4 vegades

=== VERSIO AMB COMPLETABLEFUTURE ===
  cadena declarada en 3 ms
  el fil main continua lliure; faig altres coses...
    feina del main 1
    feina del main 2
    feina del main 3
    feina del main 4
    feina del main 5
  [cadena] acabada en 1208 ms (OK)
  resultat: informes/multes.txt  (1211 ms)

La comparació correcta no és en el temps total —tots dos triguen ~1,2 s, perquè els passos són seqüencialment dependents—, sinó a la línia "cadena declarada en 3 ms". La versió blocant consumeix 1214 ms del fil principal; l'asíncrona en consumeix 3 i retorna el control. Aquell fil va poder fer altres cinc coses mentrestant. L'asincronia no accelera el que és seqüencialment dependent: allibera el fil que orquestra.

Solució a l'Exercici 2

package com.nexussoftware.bibliotech.servei;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ServeiFitxes implements AutoCloseable {

    record DadesMaterial(String titol, String autor, String isbn) {
        static DadesMaterial desconegut(String isbn) {
            return new DadesMaterial("(titol no disponible)", "(autor desconegut)", isbn);
        }
    }

    record FitxaCompleta(DadesMaterial material, int prestecs, double valoracio) {
        static FitxaCompleta minima(String isbn) {
            return new FitxaCompleta(DadesMaterial.desconegut(isbn), -1, -1.0);
        }
        @Override public String toString() {
            return String.format("%s / %s [%s] - %s prestecs - valoracio %s",
                    material.titol(), material.autor(), material.isbn(),
                    prestecs < 0 ? "?" : prestecs,
                    valoracio < 0 ? "?" : String.format("%.1f", valoracio));
        }
    }

    private final ExecutorService pool;
    private final boolean fallarValoracions;

    public ServeiFitxes(boolean fallarValoracions) {
        this.fallarValoracions = fallarValoracions;
        AtomicInteger n = new AtomicInteger(1);
        this.pool = Executors.newFixedThreadPool(8,
                r -> new Thread(r, "fitxes-" + n.getAndIncrement()));
    }

    // --- Les tres consultes, cadascuna amb la seva reserva ---

    private CompletableFuture<DadesMaterial> consultarMaterial(String isbn) {
        return CompletableFuture.supplyAsync(() -> {
            dormir(400);
            return new DadesMaterial("Java Eficac", "J. Bloch", isbn);
        }, pool)
        // Reserva INDIVIDUAL: si aquesta consulta falla, el conjunt
        // continua endavant amb un valor degradat.
        .exceptionally(e -> {
            System.err.println("  [reserva] material no disponible: " + causaDe(e).getMessage());
            return DadesMaterial.desconegut(isbn);
        });
    }

    private CompletableFuture<Integer> consultarPrestecs(String isbn) {
        return CompletableFuture.supplyAsync(() -> {
            dormir(600);                       // la mes lenta: marca el total
            return 47;
        }, pool)
        .exceptionally(e -> -1);
    }

    private CompletableFuture<Double> consultarValoracio(String isbn) {
        return CompletableFuture.supplyAsync(() -> {
            dormir(300);
            if (fallarValoracions) {
                throw new IllegalStateException("servei de valoracions caigut");
            }
            return 4.6;
        }, pool)
        .exceptionally(e -> {
            System.err.println("  [reserva] valoracio no disponible: "
                    + causaDe(e).getMessage());
            return -1.0;
        });
    }

    // --- La combinacio ---

    public CompletableFuture<FitxaCompleta> fitxaCompleta(String isbn) {

        // Les TRES es llancen alhora: l'assignacio ja dispara la feina.
        CompletableFuture<DadesMaterial> material  = consultarMaterial(isbn);
        CompletableFuture<Integer>       prestecs  = consultarPrestecs(isbn);
        CompletableFuture<Double>        valoracio = consultarValoracio(isbn);

        return material
                // thenCombine encadenat: primer material+prestecs...
                .thenCombine(prestecs, (m, p) -> new Object[] { m, p })
                // ...i despres el parell amb la valoracio.
                .thenCombine(valoracio, (parell, v) -> new FitxaCompleta(
                        (DadesMaterial) parell[0], (Integer) parell[1], v))
                // Si el conjunt triga massa, fitxa minima en lloc de fallada.
                .completeOnTimeout(FitxaCompleta.minima(isbn), 2, TimeUnit.SECONDS);
    }

    static Throwable causaDe(Throwable t) {
        return (t instanceof CompletionException || t instanceof ExecutionException)
                && t.getCause() != null ? t.getCause() : t;
    }

    static void dormir(long ms) {
        try { TimeUnit.MILLISECONDS.sleep(ms); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }

    @Override public void close() { pool.shutdown(); }

    public static void main(String[] args) throws Exception {

        System.out.println("=== CAS NORMAL ===");
        try (ServeiFitxes s = new ServeiFitxes(false)) {
            long t = System.nanoTime();
            FitxaCompleta f = s.fitxaCompleta("978-0000000001").get();
            System.out.println("  " + f);
            System.out.printf("  temps: %d ms (sequencial serien 1300)%n",
                    (System.nanoTime() - t) / 1_000_000);
        }

        System.out.println("\n=== AMB EL SERVEI DE VALORACIONS CAIGUT ===");
        try (ServeiFitxes s = new ServeiFitxes(true)) {
            long t = System.nanoTime();
            FitxaCompleta f = s.fitxaCompleta("978-0000000001").get();
            System.out.println("  " + f);
            System.out.printf("  temps: %d ms%n", (System.nanoTime() - t) / 1_000_000);
            System.out.println("  La fitxa es construeix igual: degradacio, no fallada (06-07).");
        }
    }
}

Sortida:

=== CAS NORMAL ===
  Java Eficac / J. Bloch [978-0000000001] - 47 prestecs - valoracio 4.6
  temps: 612 ms (sequencial serien 1300)

=== AMB EL SERVEI DE VALORACIONS CAIGUT ===
  [reserva] valoracio no disponible: servei de valoracions caigut
  Java Eficac / J. Bloch [978-0000000001] - 47 prestecs - valoracio ?
  temps: 608 ms
  La fitxa es construeix igual: degradacio, no fallada (06-07).

Dos resultats clau:

  1. 612 ms davant dels 1300 ms seqüencials. El total és el de la consulta més lenta (600 ms) més el cost de coordinació, perquè les tres corren en paral·lel.
  2. La fallada d'una font no tomba la fitxa. L'exceptionally individual de cada consulta la degrada a un valor per defecte i la resta es construeix normalment. És la política de 06-07 —degradar quan es pot, avortar només quan cal— portada al món asíncron. Sense aquells exceptionally individuals, la fallada de la valoració hauria fet fallar tota la combinació.

Solució a l'Exercici 3

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

public class EnviamentAvisosAsincron implements AutoCloseable {

    record Avis(String id, String empleat) { }

    record ResultatAvis(String id, boolean correcte, String detall) {
        static ResultatAvis exit(Avis a) {
            return new ResultatAvis(a.id(), true, "enviat");
        }
        static ResultatAvis fallada(Avis a, String motiu) {
            return new ResultatAvis(a.id(), false, motiu);
        }
    }

    record ResumEnviament(int total, int enviats, int fallits, long ms) {
        double percentatge() { return total == 0 ? 0 : 100.0 * enviats / total; }
    }

    private final ExecutorService poolEs;
    private final LongAdder completats = new LongAdder();

    public EnviamentAvisosAsincron() {
        AtomicInteger n = new AtomicInteger(1);
        this.poolEs = new ThreadPoolExecutor(
                16, 16, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(300),
                r -> new Thread(r, "bibliotech-avis-" + n.getAndIncrement()),
                new ThreadPoolExecutor.CallerRunsPolicy());
    }

    /** Enviament individual: 250 ms d'E/S i fallada simulada en alguns. */
    private ResultatAvis enviar(Avis a) {
        try {
            TimeUnit.MILLISECONDS.sleep(250);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new CompletionException(e);
        }
        if (a.id().hashCode() % 29 == 0) {
            throw new IllegalStateException("empleat sense adreca de contacte");
        }
        return ResultatAvis.exit(a);
    }

    /** La cadena completa. Retorna immediatament. */
    public CompletableFuture<ResumEnviament> enviarTots(List<Avis> avisos) {

        long inici = System.nanoTime();
        final int total = avisos.size();

        // 1-2. Un futur per avis, BLINDAT amb el seu propi exceptionally.
        //      Sense aquest blindatge, una sola fallada faria fallar tot l'allOf.
        List<CompletableFuture<ResultatAvis>> futurs = new ArrayList<>(total);
        for (Avis a : avisos) {
            futurs.add(CompletableFuture
                    .supplyAsync(() -> enviar(a), poolEs)
                    .exceptionally(e -> ResultatAvis.fallada(a, causaDe(e).getMessage()))
                    // Es compta en acabar, amb exit o amb fallada:
                    // aixi el main pot mostrar progres sense bloquejar-se.
                    .whenComplete((r, e) -> completats.increment()));
        }

        // 3. Agregar.
        CompletableFuture<Void> tots =
                CompletableFuture.allOf(futurs.toArray(new CompletableFuture[0]));

        return tots
                // 4. Recollir i resumir.
                .thenApply(v -> {
                    int ok = 0, ko = 0;
                    for (CompletableFuture<ResultatAvis> f : futurs) {
                        // join() no bloqueja: allOf garanteix que estan complets.
                        if (f.join().correcte()) ok++; else ko++;
                    }
                    return new ResumEnviament(total, ok, ko,
                            (System.nanoTime() - inici) / 1_000_000);
                })
                // 5. Termini global i gestor final.
                .orTimeout(60, TimeUnit.SECONDS)
                .exceptionally(e -> {
                    System.err.println("Enviament massiu fallit: " + causaDe(e).getMessage());
                    return new ResumEnviament(total, 0, total,
                            (System.nanoTime() - inici) / 1_000_000);
                });
    }

    public long completats() { return completats.sum(); }

    static Throwable causaDe(Throwable t) {
        return (t instanceof CompletionException || t instanceof ExecutionException)
                && t.getCause() != null ? t.getCause() : t;
    }

    @Override public void close() {
        poolEs.shutdown();
        try {
            if (!poolEs.awaitTermination(30, TimeUnit.SECONDS)) poolEs.shutdownNow();
        } catch (InterruptedException e) {
            poolEs.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) throws Exception {

        List<Avis> avisos = new ArrayList<>();
        for (int i = 1; i <= 200; i++) {
            avisos.add(new Avis("AV-" + i, "empleat-" + (i % 20)));
        }

        try (EnviamentAvisosAsincron servei = new EnviamentAvisosAsincron()) {

            System.out.println("=== ENVIAMENT ASINCRON DE 200 AVISOS ===");
            System.out.println("Sequencial serien " + (200 * 250 / 1000) + " s\n");

            // 6. La cadena es declara i retorna: main NO es bloqueja.
            CompletableFuture<ResumEnviament> cadena = servei.enviarTots(avisos);

            // Progres, llegint el LongAdder que les tasques incrementen.
            while (!cadena.isDone()) {
                long fets = servei.completats();
                System.out.printf("\r  [%s] %d/200 (%.0f%%)",
                        barra(fets, 200), fets, 100.0 * fets / 200);
                TimeUnit.MILLISECONDS.sleep(250);
            }

            ResumEnviament r = cadena.get();
            System.out.printf("\r  [%s] 200/200 (100%%)%n%n", barra(200, 200));
            System.out.println("=== RESUM ===");
            System.out.println("Total     : " + r.total());
            System.out.println("Enviats   : " + r.enviats());
            System.out.println("Fallits   : " + r.fallits());
            System.out.println("Temps     : " + r.ms() + " ms");
            System.out.printf ("Exit      : %.1f%%%n", r.percentatge());
            System.out.printf ("Acceleracio: %.1fx%n", 200 * 250.0 / r.ms());
        }
    }

    static String barra(long fets, long total) {
        int amplada = 30;
        int plens = (int) (amplada * fets / total);
        return "#".repeat(plens) + "-".repeat(amplada - plens);
    }
}

Sortida:

=== ENVIAMENT ASINCRON DE 200 AVISOS ===
Sequencial serien 50 s

  [##############################] 200/200 (100%)

=== RESUM ===
Total     : 200
Enviats   : 193
Fallits   : 7
Temps     : 3387 ms
Exit      : 96.5%
Acceleracio: 14.8x

Quatre punts:

  1. 14,8× d'acceleració amb 16 fils. No és 16× pel cost de coordinació i perquè l'última tanda no omple tots els fils. 200 × 250 ms / 16 ≈ 3,1 s, molt a prop del resultat.
  2. L'exceptionally individual és essencial. Sense ell, una de les set fallades hauria fet fallar l'allOf complet i el resum hauria estat 0 enviats / 200 fallits. Blindar cada futur abans d'agregar-lo és el patró obligatori.
  3. El LongAdder incrementat a whenComplete permet el progrés sense bloquejar. El main llegeix completats() cada 250 ms mentre la cadena avança sola. És 08-06 i 08-07 treballant junts.
  4. join() dins del thenApply no bloqueja res, perquè allOf ja va garantir que tots els futurs estan complets. És l'únic lloc on join() és inofensiu dins d'una etapa.

Conclusió

Has tancat el mòdul 8. Vas començar amb dos fils que perdien mig milió d'increments i acabes amb una aplicació que fa diverses coses alhora, de forma correcta i sense que ningú esperi ningú.

En aquesta lliçó has vist per què Future es va quedar curt: no es pot encadenar, no es pot combinar, get() bloqueja, no admet callbacks i no es pot completar a mà. I has après el model que el substitueix: en lloc de preguntar i esperar, declarar la cadena sencera per endavant i deixar que cada etapa es dispari quan l'anterior li lliuri un valor.

Saps crear un CompletableFuture amb supplyAsync quan produeix un valor, runAsync quan no, completedFuture per unificar el camí ràpid i el lent, i el constructor buit per completar-lo a mà. I coneixes la decisió que més conseqüències té: l'executor. Per defecte es fa servir el ForkJoinPool.commonPool(), amb nuclis - 1 fils compartits per tota la JVM —inclosos els streams paral·lels de 10-04—, així que per a qualsevol tasca que bloquegi cal passar un executor propi, dimensionat segons 08-05 i amb fils anomenats segons 08-02. Amb la sorpresa que convé recordar: en una màquina d'un sol nucli, el pool comú té paral·lelisme zero i el teu codi "asíncron" es torna síncron sense avisar.

Domines la transformació amb thenApply, thenAccept i thenRun —els tipus funcionals de 04-06 aplicats—, i sobretot la distinció que més confon: thenApply quan la funció retorna un valor, thenCompose quan retorna un altre CompletableFuture, amb el CompletableFuture<CompletableFuture<T>> que apareix en equivocar-se i l'analogia que ho fixa: map davant de flatMap. Saps què canvien les variants ...Async —el fil que executa l'etapa— i la regla derivada: sense sufix per a transformacions barates, amb sufix i executor propi per a la feina cara, perquè una etapa sense sufix pot acabar executant-se al fil que va completar l'anterior o fins i tot a main.

Saps combinar: thenCombine per ajuntar dos resultats que es calculen en paral·lel —de manera que el temps total és el del més lent, no la suma—, i allOf/anyOf per a N futurs, amb el patró de recollida —allOf i després un thenApply que crida join(), segur perquè ja estan complets— i el parany que cal evitar: blindar cada futur amb el seu propi exceptionally abans d'agregar-lo, o una única fallada tombarà el conjunt.

I gestiones els errors en un món on no hi ha try/catch que valgui: exceptionally per recuperar amb un valor de reserva, handle per unificar els dos camins, i whenComplete per observar sense alterar —el que es fa servir per a mètriques i traces—. Saps com viatja una excepció: curtcircuita totes les etapes següents fins al primer gestor, arriba embolcallada en CompletionException amb la causa real a getCause(), i després d'un exceptionally la cadena continua amb normalitat. Amb els dos errors clàssics ben identificats: posar el gestor massa amunt, on només cobreix l'anterior, i no posar-ne cap, amb la qual cosa la fallada desapareix sense traça, sense log i sense excepció — el mateix forat que submit sense get() a 08-05.

Coneixes orTimeout i completeOnTimeout de Java 9, amb l'avís imprescindible que cap dels dos no cancel·la la feina subjacent; el completat manual amb complete i completeExceptionally per adaptar una API de callbacks sense tocar-la; i el límit real de la cancel·lació: cancel(true) ignora el seu paràmetre i no interromp res, així que la cancel·lació de veritat continua sent el marcador compartit i els punts de comprovació de 08-02.

BiblioTech, en tancar el mòdul 8, ha deixat d'esperar.

La seva importació de catàleg corre al seu propi fil amb nom propi, publica el seu progrés, es comprova cancel·lable una vegada per línia i —l'essencial— construeix una llista a part que només bolca al catàleg si acaba, de manera que una cancel·lació no deixa mai l'estat a mitges. Els seus dos-cents avisos s'envien amb un pool acotat de vuit fils i fils anomenats, amb barra de progrés mitjançant CountDownLatch, amb un semàfor que limita a cinc els accessos simultanis al fitxer, amb cada fallada individual registrada sense aturar el lot i amb apagada en dues fases. El seu catàleg és segur per a diversos fils —primer amb ReadWriteLock, després amb ConcurrentHashMap i CopyOnWriteArrayList— i aguanta un milió sis-centes mil operacions amb setze fils en tres-cents mil·lisegons sense un sol lock() explícit. El seu registre de préstecs manté l'invariant entre els seus dos mapes sota un únic pany, perquè això és l'única cosa que un invariant entre estructures admet, i exposa les seves operacions compostes com a mètodes atòmics per no regalar curses a qui el faci servir. Les seves estadístiques són LongAdder i AtomicReference sobre record immutables: lectures gratuïtes, escriptures atòmiques, zero interbloquejos possibles. La seva cua de reserves és un productor-consumidor real amb ArrayBlockingQueue, amb contrapressió visible i apagada per píndola verinosa sense perdre ni una reserva. I el seu informe mensual es genera amb una cadena asíncrona que carrega dues fonts en paral·lel, calcula al pool de CPU, escriu al pool d'E/S, té termini, mètriques i gestor final — mentre el menú atén vuit opcions sense aturar-se ni un instant.

De les quatre mancances que vas declarar en tancar el mòdul 7 no en queda cap: la importació ja no bloqueja el menú, els avisos ja no van d'un en un, el processador ja no està aturat esperant una tecla, i dos empleats treballant alhora ja no poden corrompre res.

Però tot això passa dins d'una sola màquina. BiblioTech és un programa que s'executa en un ordinador i fa servir els seus nuclis, la seva memòria i els seus fitxers. La Marta Ruiz només el pot consultar si s'asseu davant d'aquell ordinador. En Diego Alonso, des d'una altra planta de l'edifici, no pot. No hi ha forma que el catàleg es comparteixi entre les tres seus de Nexus Software, ni que un empleat consulti la disponibilitat d'un llibre des del seu portàtil, ni que BiblioTech pregunti a un servei extern l'ISBN d'una novetat o enviï de veritat aquells avisos que fins ara només escriu en un fitxer local. Tota la concurrència que has après reparteix feina entre fils del mateix procés; res del que saps fins ara no permet repartir-la entre màquines.

I hi ha una asimetria que crida l'atenció: has après a solapar l'espera d'E/S d'un disc, que triga mil·lisegons, mentre que l'espera de la xarxa triga centenars de mil·lisegons i és exactament on més rendeix tot això. Els pools dimensionats per a E/S, CompletableFuture, BlockingQueue i la cancel·lació cooperativa van ser dissenyats pensant sobretot en la xarxa.

Al mòdul 9, Xarxes, BiblioTech surt de la seva màquina. Veuràs què hi ha realment sota d'una connexió —adreces IP, ports, el model per capes i la diferència entre TCP i UDP—; els sockets com a extrems d'una conversa entre dos programes, amb Socket al client i ServerSocket al servidor, i un servidor que atén diversos clients alhora amb exactament els pools que acabes d'aprendre; DatagramSocket per quan la velocitat importa més que la garantia de lliurament; l'accés a recursos web amb URL i HttpURLConnection; i el client HTTP modern de Java 11, el sendAsync del qual retorna —no és casualitat— un CompletableFuture. En acabar-lo, el catàleg de BiblioTech es consultarà des de qualsevol ordinador de Nexus Software, i l'aplicació podrà parlar amb serveis externs. Deixarà d'estar sola.

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