Salta al contenuto
Note per Studenti Concorrenza in Java - thread, sincronizzazione e pool

Concorrenza in Java - thread, sincronizzazione e pool

In questa pagina 8

I concetti generali di thread, race condition, mutua esclusione e sincronizzazione sono in Thread e concorrenza - modelli, Pthreads e goroutineUn thread è un flusso di esecuzione dentro un processo: ha program counter, registri e stack propri ma condivide codice, dati, heap e file aperti del processo, quindi costa meno di un processo (creazione e commutazione) e comunica più facilmente, ma i dati condivisi producono race condition; concorrenza (più compiti che avanzano, anche su un solo core) non è parallelismo (più compiti eseguiti nello stesso istante, serve più di un core); si distinguono parallelismo sui dati e sui compiti e la legge di Amdahl dà lo speedup massimo 1/(S+(1-S)/N) con S frazione seriale; i thread utente sono gestiti da una libreria, quelli kernel dal sistema, e i modelli many-to-one, one-to-one (Linux, Windows) e many-to-many li collegano; in C si usa Pthreads (pthread_create, pthread_join, pthread_exit), in Go le goroutine (parola chiave go, sync.WaitGroup, canali); questioni aperte: fork/exec con i thread, segnali, cancellazione (asincrona o differita), thread-local storage, contention scope (PCS e SCS).Thread e concorrenza - modelli, Pthreads e goroutine → e in Semafori, monitor e problemi classici di sincronizzazioneUn semaforo è un intero accessibile solo con due operazioni atomiche, wait (P: aspetta finché S > 0 e decrementa) e signal (V, in POSIX sem_post: incrementa); il semaforo contatore ha valori illimitati, quello binario vale 0 o 1; con valore iniziale 1 realizza la mutua esclusione, con valore iniziale 0 impone un ordine di esecuzione (S1 prima di S2); senza attesa attiva ogni semaforo ha una coda di processi sospesi (sleep e wakeup); i semafori si usano male con facilità (ordine sbagliato, omissioni); il monitor è un tipo di dato astratto in cui un solo processo alla volta è attivo e le variabili condizione x.wait()/x.signal() sospendono e risvegliano (in POSIX mutex più pthread_cond_t); le anomalie di liveness sono deadlock, starvation e inversione di priorità; i problemi classici sono produttore-consumatore a buffer limitato (mutex, vuote = n, piene = 0; l'ordine delle wait è importante), lettori-scrittori (rw_mutex, mutex, read_count; i lettori possono far morire di fame gli scrittori) e filosofi a cena (la soluzione ingenua con un semaforo per bacchetta va in deadlock; si evita limitando a n-1 filosofi, rompendo la simmetria o ordinando le risorse), con soluzioni in C (POSIX) e in Go (canali e sync).Semafori, monitor e problemi classici di sincronizzazione →: questa nota li traduce in Java e li usa per gli esempi del corso (MyThread1, MyThread2, MyPool) e per i pattern concorrenti del programma. Tutti i programmi sono stati compilati ed eseguiti con JDK 21; l'ordine di esecuzione dei thread dipende dallo scheduler, quindi nelle uscite si riportano solo le parti deterministiche.

Creare e avviare thread

Definizione. Un thread Java è un flusso di esecuzione di un oggetto Thread; il codice che esegue è il metodo run() di un Runnable. Tutti i thread di una JVM condividono lo stesso heap (ognuno ha il proprio stack).

Due modi equivalenti: ereditare da Thread e ridefinire run(), oppure (preferibile: si può ancora ereditare da altro) implementare Runnable e passarlo a un Thread:

java
Runnable lavoro = () -> System.out.println("ciao da " + Thread.currentThread().getName());
Thread t = new Thread(lavoro, "t1");
t.start();       // crea il thread: la JVM chiama run() nel NUOVO thread
t.join();        // il chiamante aspetta la fine del thread
// lavoro.run() chiamato direttamente eseguirebbe il codice nel thread corrente, senza concorrenza
  • Il programma termina quando finiscono tutti i thread non daemon; il main può finire prima degli altri (il corso stampa "terminates" subito, senza aspettare: manca la join).
  • join() aspetta la fine di un thread, sleep(ms) sospende il thread corrente (lancia InterruptedException), yield() suggerisce allo scheduler di passare ad altri, interrupt() chiede a un thread di fermarsi: i metodi bloccanti reagiscono lanciando InterruptedException, e chi la cattura non deve ingoiarla (Thread.currentThread().interrupt() per rimettere il flag).
  • setDaemon(true) (prima di start) rende il thread "di servizio": non impedisce la chiusura del programma.

L'esempio del corso MyThread2

MyThread2 implementa Runnable e crea un thread per ogni argomento della riga di comando, tutti sullo stesso oggetto (new Thread(this, argv[i])): quindi il campo k è condiviso. Nella versione sotto (con join e meno giri) la stampa e l'incremento di k stanno dentro synchronized(this), così i valori stampati sono sempre 0, 1, 2... senza buchi né ripetizioni, anche se i nomi dei thread si alternano in modo diverso a ogni esecuzione. MyThread1, senza campo condiviso, mostra solo l'intreccio delle stampe.

java
/** Esempio del corso (MyThread2): piu' thread eseguono lo stesso Runnable e condividono il campo k. */
public class MyThread2 implements Runnable {
    int k = 0;                                   // condiviso: tutti i thread usano lo stesso oggetto (this)

    public static void main(String[] argv) throws InterruptedException {
        new MyThread2().exec(argv);
    }

    public void exec(String[] argv) throws InterruptedException {
        Thread[] t = new Thread[argv.length];
        for (int i = 0; i < argv.length; i++) {
            t[i] = new Thread(this, argv[i]);    // stesso Runnable, nome diverso
            t[i].start();                        // start() crea il thread e chiama run() in esso; run() direttamente NO
        }
        for (Thread x : t) x.join();             // il corso non fa join: il main termina prima degli altri
        System.out.println(Thread.currentThread().getName() + " termina, k = " + k);
    }

    public void run() {
        for (int i = 0; i < 4; i++) {
            synchronized (this) {                // lettura, stampa e incremento di k come un'unica sezione critica
                System.out.println("thread " + Thread.currentThread().getName() + " val " + k);
                k = k + 1;
            }
            Thread.yield();                      // suggerisce allo scheduler di far girare un altro thread
        }
    }
}

Uscita con argomenti A B C (i nomi cambiano tra le esecuzioni, la sequenza di val no):

thread A val 0
thread A val 1
thread A val 2
thread A val 3
thread B val 4
thread B val 5
thread B val 6
thread C val 7
thread B val 8
thread C val 9
thread C val 10
thread C val 11
main termina, k = 12

Race condition e mutua esclusione

senza++ sembra un'istruzione ma sono tre passi (leggi, somma, scrivi): se due thread li intrecciano, un aggiornamento va perso. Il programma sotto fa incrementare 200000 volte lo stesso contatore da due thread in tre modi.

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

/** Due thread incrementano lo stesso contatore: senza sincronizzazione si perdono aggiornamenti. */
public class CorsaCritica {
    static int senza = 0;                         // nessuna protezione
    static int con = 0;                           // protetto da synchronized
    static final Object lucchetto = new Object();
    static final AtomicInteger atomico = new AtomicInteger();

    static final int GIRI = 200_000;

    public static void main(String[] args) throws InterruptedException {
        Runnable lavoro = () -> {
            for (int i = 0; i < GIRI; i++) {
                senza++;                          // lettura, somma, scrittura: 3 passi separabili
                synchronized (lucchetto) { con++; }
                atomico.incrementAndGet();        // un'unica operazione atomica
            }
        };
        Thread t1 = new Thread(lavoro, "t1");
        Thread t2 = new Thread(lavoro, "t2");
        t1.start();
        t2.start();
        t1.join();                                // il main aspetta la fine dei due thread
        t2.join();
        System.out.println("atteso      = " + 2 * GIRI);
        System.out.println("senza sync  = " + senza + (senza == 2 * GIRI ? "" : "   <-- aggiornamenti persi"));
        System.out.println("synchronized= " + con);
        System.out.println("AtomicInteger= " + atomico.get());
    }
}

Uscita (il valore senza sincronizzazione cambia a ogni esecuzione, ma è quasi sempre inferiore all'atteso):

atteso      = 400000
senza sync  = 373712   <-- aggiornamenti persi
synchronized= 400000
AtomicInteger= 400000
meccanismo come funziona quando
synchronized (metodo o blocco) ogni oggetto ha un monitor (lock intrinseco); un solo thread alla volta lo possiede; è rientrante (lo stesso thread può riacquisirlo) sezioni critiche brevi; il blocco synchronized(obj) permette di scegliere l'oggetto-lucchetto
AtomicInteger, AtomicLong, ... operazioni atomiche senza lock (istruzioni dell'hardware) contatori e flag semplici
ReentrantLock (java.util.concurrent.locks) lock esplicito con lock()/unlock() (in finally), tryLock, condizioni quando servono attese con tempo massimo o più condizioni
volatile garantisce la visibilità (un thread vede la scrittura di un altro) ma non l'atomicità di x++ flag di arresto, campo del double-checked locking
oggetti immutabili (final, String) nessuno li modifica: nessun problema dati condivisi in sola lettura

Regole pratiche: proteggere ogni accesso allo stesso dato con lo stesso lock (leggere senza lock un dato scritto con lock è comunque un errore di visibilità); tenere le sezioni critiche corte; non chiamare codice sconosciuto mentre si tiene un lock.

Attesa di una condizione: wait e notifyAll

Quando un thread deve aspettare che un'altra condizione diventi vera (una copia disponibile, un posto libero nel buffer) non deve fare busy waiting: usa wait(), che rilascia il monitor e sospende il thread finché un altro chiama notify() o notifyAll() sullo stesso oggetto.

Regole (se si violano, si hanno errori sottili):

  1. wait, notify, notifyAll si chiamano solo dentro synchronized sullo stesso oggetto (altrimenti IllegalMonitorStateException);
  2. wait va sempre in un ciclo while che ricontrolla la condizione: dopo il risveglio un altro thread può averla resa di nuovo falsa (e ci sono risvegli spuri);
  3. in caso di dubbio notifyAll (con notify si risveglia un solo thread, a caso, che potrebbe non essere quello giusto);
  4. wait lancia InterruptedException.

Il pool di risorse (MyPool del corso)

Il file MyPool.java del corso contiene un monitor Pool con get(n) (attende se n > risorse disponibili) e release(n) (restituisce e sveglia). Il file non compila e va corretto in più punti: la classe Pool è in un file chiamato MyPool.java mentre è dichiarata public class Pool; i costruttori sono scritti public pool(...) (minuscolo: sembrano metodi senza tipo di ritorno); this.wait() lancia InterruptedException, che è controllata e va dichiarata o catturata; pool += howMany(); chiama come metodo un parametro int; e currentPool legge pool senza sincronizzazione. Versione corretta (prendi e rilascia), con cinque lettori e due copie:

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

/** Monitor: un insieme di risorse identiche (per esempio le copie di un libro). */
class Pool {
    private int disponibili;

    Pool(int iniziali) { disponibili = iniziali; }

    /** Prende n risorse; se non bastano, il thread attende. */
    synchronized void prendi(int n) throws InterruptedException {
        while (n > disponibili) {                // while, mai if: dopo il risveglio si ricontrolla
            wait();
        }
        disponibili -= n;
    }

    /** Restituisce n risorse e sveglia chi aspetta. */
    synchronized void rilascia(int n) {
        disponibili += n;
        notifyAll();
    }

    synchronized int disponibili() { return disponibili; }
}

public class PoolRisorse {
    static final AtomicInteger dentro = new AtomicInteger();
    static final AtomicInteger massimo = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Pool pool = new Pool(2);
        Thread[] lettori = new Thread[5];
        for (int i = 0; i < lettori.length; i++) {
            final int id = i;
            lettori[i] = new Thread(() -> {
                try {
                    pool.prendi(1);                       // al massimo 2 lettori contemporanei
                    massimo.accumulateAndGet(dentro.incrementAndGet(), Math::max);
                    Thread.sleep(20);                     // il lettore "usa" la copia
                    dentro.decrementAndGet();
                    pool.rilascia(1);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }, "lettore-" + id);
            lettori[i].start();
        }
        for (Thread t : lettori) t.join();
        System.out.println("lettori contemporanei al massimo: " + massimo.get());
        System.out.println("copie disponibili alla fine: " + pool.disponibili());
    }
}

Uscita:

lettori contemporanei al massimo: 2
copie disponibili alla fine: 2

Al massimo due lettori sono dentro insieme, e al termine le due copie sono tornate nel pool: la condizione n > disponibili è l'invariante del monitor.

Produttore-consumatore

Schema classico: un buffer limitato, i produttori attendono se è pieno, i consumatori se è vuoto. Un unico monitor (il buffer) con wait/notifyAll; il produttore segnala la fine con un valore speciale ("pillola avvelenata").

java
import java.util.LinkedList;
import java.util.Queue;

/** Buffer limitato con wait/notifyAll: i produttori attendono se e' pieno, i consumatori se e' vuoto. */
class Buffer<T> {
    private final Queue<T> coda = new LinkedList<>();
    private final int capacita;

    Buffer(int capacita) { this.capacita = capacita; }

    synchronized void metti(T x) throws InterruptedException {
        while (coda.size() == capacita) wait();
        coda.add(x);
        notifyAll();                              // sveglia i consumatori (e gli altri produttori)
    }

    synchronized T prendi() throws InterruptedException {
        while (coda.isEmpty()) wait();
        T x = coda.remove();
        notifyAll();
        return x;
    }
}

public class ProduttoreConsumatore {
    static final int FINE = -1;                   // "pillola avvelenata": dice al consumatore di fermarsi

    public static void main(String[] args) throws InterruptedException {
        Buffer<Integer> buf = new Buffer<>(3);
        long[] somma = new long[1];

        Thread produttore = new Thread(() -> {
            try {
                for (int i = 1; i <= 100; i++) buf.metti(i);
                buf.metti(FINE);
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });
        Thread consumatore = new Thread(() -> {
            try {
                int x;
                while ((x = buf.prendi()) != FINE) somma[0] += x;
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });
        produttore.start();
        consumatore.start();
        produttore.join();
        consumatore.join();
        System.out.println("somma consumata = " + somma[0] + " (atteso 5050)");
    }
}
somma consumata = 5050 (atteso 5050)

In pratica si usa java.util.concurrent.ArrayBlockingQueue/LinkedBlockingQueue, che fanno già tutto questo (put e take bloccanti).

Stallo (deadlock)

Definizione. Due o più thread sono in stallo quando ciascuno aspetta un lock posseduto da un altro, e nessuno può procedere.

Le condizioni per lo stallo sono quattro (mutua esclusione, possesso e attesa, assenza di prelazione, attesa circolare); in Java il caso tipico è l'ordine opposto di acquisizione di due lock. Nel programma t1 prende A poi B, t2 prende B poi A: ognuno ha il primo e aspetta il secondo. I thread sono daemon per poter chiudere il programma. Rimedio: un ordine globale dei lock (tutti prendono A prima di B).

java
/** Due thread prendono due lucchetti in ordine opposto: stallo (deadlock). Qui si evita ordinandoli. */
public class Stallo {
    static final Object A = new Object(), B = new Object();

    static void bloccante(Object primo, Object secondo, String nome) {
        synchronized (primo) {
            try { Thread.sleep(50); } catch (InterruptedException e) { return; }   // forza l'intreccio
            synchronized (secondo) {
                System.out.println(nome + " ha preso entrambi");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        boolean ordinato = args.length > 0 && args[0].equals("ordinato");
        Thread t1 = new Thread(() -> bloccante(A, B, "t1"));
        // t2: ordine inverso => stallo; ordine uguale a t1 => nessuno stallo
        Thread t2 = new Thread(() -> { if (ordinato) bloccante(A, B, "t2"); else bloccante(B, A, "t2"); });
        t1.setDaemon(true);                       // i thread bloccati non impediscono la chiusura del programma
        t2.setDaemon(true);
        t1.start();
        t2.start();
        t1.join(1000);
        t2.join(1000);
        System.out.println(t1.isAlive() || t2.isAlive() ? "STALLO: i thread sono ancora bloccati" : "terminati");
    }
}

Uscita senza argomenti (ordine opposto) e con l'argomento ordinato:

STALLO: i thread sono ancora bloccati
t1 ha preso entrambi
t2 ha preso entrambi
terminati

Thread pool e ExecutorService

Creare un thread per ogni compito è costoso e non limita il carico. Un thread pool riusa un numero fisso di thread che prelevano i compiti da una coda. Executors.newFixedThreadPool(n) crea n thread; submit accetta un Runnable o un Callable<T> (un compito che restituisce un valore e può lanciare eccezioni) e restituisce un Future<T>, il cui get() attende il risultato.

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

/** Thread pool: pochi thread riutilizzati per molti compiti; Future restituisce il risultato. */
public class PoolDiThread {
    // compito: contare i numeri primi in [da, a)
    static int primi(int da, int a) {
        int n = 0;
        for (int x = Math.max(da, 2); x < a; x++) {
            boolean p = true;
            for (int d = 2; (long) d * d <= x; d++) if (x % d == 0) { p = false; break; }
            if (p) n++;
        }
        return n;
    }

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(4);   // 4 thread per 8 compiti
        List<Future<Integer>> parziali = new ArrayList<>();
        for (int k = 0; k < 8; k++) {
            final int da = k * 25_000, a = da + 25_000;
            Callable<Integer> compito = () -> primi(da, a);
            parziali.add(pool.submit(compito));
        }
        int totale = 0;
        for (Future<Integer> f : parziali) totale += f.get();    // get() attende il risultato
        pool.shutdown();                                           // niente nuovi compiti
        pool.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("primi sotto 200000 = " + totale + " (sequenziale: " + primi(0, 200_000) + ")");
    }
}
primi sotto 200000 = 17984 (sequenziale: 17984)

Con shutdown() il pool non accetta altri compiti e termina quando ha finito quelli accodati (awaitTermination attende); dimenticarlo lascia i thread vivi e il programma non termina.

Elementi di pattern concorrenti

pattern idea in questa nota
monitor (monitor object) i dati condivisi e le operazioni che li usano stanno in un oggetto con synchronized/wait/notifyAll Pool, Buffer
produttore-consumatore disaccoppia chi genera da chi elabora con un buffer limitato Buffer<T>
thread pool pochi thread riusati, coda di compiti ExecutorService
future il risultato di un calcolo asincrono, da leggere dopo Future<Integer>
Singleton thread-safe una sola istanza anche con più thread: eager, holder, double-checked locking con volatile, enum Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →
Observer concorrente lista di osservatori che si può modificare durante la notifica (CopyOnWriteArrayList) Pattern comportamentali - Observer, Strategy, Command, Iterator e Template MethodI pattern comportamentali descrivono come gli oggetti collaborano e si dividono le responsabilità: Observer (publish-subscribe) avvisa automaticamente i dipendenti quando il soggetto cambia stato, mantenendo basso l'accoppiamento (si registrano e cancellano a runtime, modello push o pull, attenzione alle modifiche durante la notifica); Strategy rende intercambiabili gli algoritmi dietro un'interfaccia (in Java anche con una lambda); Command trasforma la richiesta in un oggetto, quindi memorizzabile, accodabile e annullabile (undo); Iterator scorre un aggregato senza esporne la struttura (for-each, fail-fast); Template Method fissa lo scheletro di un algoritmo e lascia alle sottoclassi passi astratti o hook (principio di Hollywood); State, Visitor, Chain of Responsibility, Mediator, Memento e Interpreter completano le undici forme del catalogo.Pattern comportamentali - Observer, Strategy, Command, Iterator e Template Method →
oggetto immutabile niente stato modificabile, niente lock String, record

Un esercizio completo di sincronizzazione su una risorsa limitata (le copie di un libro) è in Esercizio - concorrenza sulle copie di un libro con monitor e attesa limitata (progetto originale).

Domande d'esame

1. Perché contatore++ eseguito da due thread può dare un risultato sbagliato? Come si corregge? Traccia. È lettura, somma e scrittura: se i passi di due thread si intrecciano, un aggiornamento si perde (race condition). Si corregge rendendo l'operazione una sezione critica (synchronized sullo stesso lock per ogni accesso) o usando AtomicInteger; volatile non basta perché non rende atomico l'incremento.

2. Perché wait() va chiamato in un while e dentro un blocco synchronized? Traccia. wait rilascia il monitor e va chiamato mentre lo si possiede (altrimenti IllegalMonitorStateException). Dopo il risveglio un altro thread può aver reso falsa la condizione (o il risveglio è spurio): il while la ricontrolla. notifyAll evita di risvegliare il thread sbagliato.

3. Fare un esempio di stallo con due lock e dire come evitarlo. Traccia. t1 prende A poi B, t2 prende B poi A: ognuno possiede un lock e aspetta quello dell'altro. Si evita con un ordine globale di acquisizione (sempre A poi B), oppure con tryLock a tempo, oppure riducendo i lock.

Versione ripasso

  • Thread: oggetto Thread che esegue run() di un Runnable; start() crea il thread (chiamare run() no); join() attende; sleep/yield/interrupt; setDaemon(true) prima di start. Tutti i thread condividono l'heap. Il programma termina quando finiscono i thread non daemon. Non ingoiare InterruptedException.
java
Thread t = new Thread(() -> System.out.println("ciao"), "t1");
t.start();
t.join();
  • Race condition: x++ = leggi, somma, scrivi: gli aggiornamenti si perdono (200000 incrementi per 2 thread: senza sync si ottiene meno di 400000, con synchronized o AtomicInteger esattamente 400000).
java
Runnable lavoro = () -> {
    for (int i = 0; i < 200_000; i++) {
        senza++;                                   // 3 passi: aggiornamenti persi
        synchronized (lucchetto) { con++; }        // sezione critica: sempre 400000
        atomico.incrementAndGet();                 // atomico: sempre 400000
    }
};
  • Regole di wait: (1) dentro synchronized sullo stesso oggetto, altrimenti IllegalMonitorStateException; (2) in un while (altri thread possono rendere falsa la condizione, risvegli spuri); (3) nel dubbio notifyAll; (4) lancia InterruptedException.
  • Meccanismi: synchronized (monitor, rientrante), blocco synchronized(obj), AtomicInteger, ReentrantLock (lock/unlock in finally, tryLock), volatile (visibilità, non atomicità), oggetti immutabili. Stesso lock per ogni accesso; sezioni critiche corte.
  • wait/notify: solo dentro synchronized sullo stesso oggetto; wait rilascia il monitor; sempre in while (!condizione) wait();; notifyAll nel dubbio.
java
synchronized void prendi(int n) throws InterruptedException {
    while (n > disponibili) wait();
    disponibili -= n;
}
synchronized void rilascia(int n) { disponibili += n; notifyAll(); }
  • MyPool del corso non compila: classe Pool in MyPool.java; costruttori public pool(...); this.wait() senza gestire InterruptedException; pool += howMany(); (parametro chiamato come metodo); currentPool non sincronizzato. Corretto sopra: con 5 lettori e 2 copie al massimo 2 lettori sono dentro insieme e alla fine le copie tornano 2.
  • Produttore-consumatore: buffer limitato con un solo monitor; metti attende se pieno, prendi se vuoto, entrambi notifyAll; fine con "pillola avvelenata" (-1); somma 1..100 = 5050. In pratica: ArrayBlockingQueue (put/take).
java
synchronized void metti(T x) throws InterruptedException {
    while (coda.size() == capacita) wait();     // pieno: il produttore aspetta
    coda.add(x);
    notifyAll();
}
synchronized T prendi() throws InterruptedException {
    while (coda.isEmpty()) wait();              // vuoto: il consumatore aspetta
    T x = coda.remove();
    notifyAll();
    return x;
}
  • Stallo: t1 prende A poi B, t2 B poi A; condizioni: mutua esclusione, possesso e attesa, no prelazione, attesa circolare. Rimedio: ordine globale dei lock, tryLock a tempo. Il programma di prova stampa STALLO: i thread sono ancora bloccati con l'ordine opposto e t1 ha preso entrambi, t2 ha preso entrambi, terminati con l'argomento ordinato.
  • Thread pool: Executors.newFixedThreadPool(n); submit(Callable<T>) restituisce Future<T>; get() attende; shutdown() + awaitTermination (senza shutdown il programma non termina). Esempio: 8 compiti su 4 thread contano i primi sotto 200000: 17984, uguale al sequenziale.
java
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> primi(0, 25_000));   // Callable
int n = f.get();                                           // attende il risultato
pool.shutdown();

Esercizi su questo argomento

Teoria collegata