Lezione 15 · Canale 1 · lunedì 2 novembre 2026
Thread e code bloccanti in Java
Progettazione del Software
Lezione 15 · Canale 1 · lunedì 2 novembre 2026
Progettazione del Software
Un elevatore è comandato da due server separati, che non comunicano fra loro. Il server del motore lo accende per un certo tempo, ma non sa di quanto l'elevatore salga: il motore non è preciso, e a ogni comando sale di una quantità un po' diversa. Il server del rilevatore dice soltanto l'altezza attuale. Li collega solo il movimento meccanico, perché quando il motore alza l'elevatore cambia l'altezza che il rilevatore misura.
Tutta la logica sta nel client, un programma con interfaccia grafica in cui l'utente chiede di alzare o abbassare. Il client non può spedire di seguito tutti i comandi: prima di ognuno deve conoscere l'altezza, che dipende dal comando precedente. Il socket consegna i dati in ordine, ma non sa niente dell'altezza, quindi la regola sta nel client. Per ogni richiesta di sollevamento il protocollo è questo:
rileva;Solo allora il client passa alla richiesta successiva. Intanto l'utente può chiedere altri sollevamenti, e l'interfaccia non deve bloccarsi. Le richieste vanno quindi accumulate e servite una per volta, in ordine.
Un thread per ogni pressione di pulsante non basta: i thread procederebbero insieme, mentre ogni richiesta deve aspettare la fine della precedente. Serve un solo thread che le serva una dopo l'altra, come una coda di stampa: un programma riceve i file, li mette in coda e li manda uno per volta alla stampante, mentre l'utente continua a lavorare.
In molti programmi alcuni oggetti hanno un comportamento che evolve nel tempo, indipendente da quello degli altri. Un esempio: persone che prenotano appuntamenti presso uffici. Una persona ha al più un appuntamento per volta, mentre un ufficio ne gestisce quanti ne servono.
Lo schema dei dati
Ogni persona è in uno stato: registrata, in attesa della data dopo aver chiesto un appuntamento, oppure con la data fissata in attesa dello svolgimento. Cambia stato in modo indipendente dalle altre persone e dagli uffici. Anche ogni ufficio ha il suo processo, e persone e uffici comunicano con messaggi: la persona chiede, l'ufficio fissa la data. Gli appuntamenti invece non compiono azioni: sono solo dati.
Un oggetto con un processo indipendente ha un proprio thread. Se il client di una persona bloccasse tutto, fermerebbe le altre persone e gli uffici. Quindi c'è un thread per ogni persona e per ogni ufficio, e nessuno per gli appuntamenti. Lo stesso vale per musicisti che si mandano messaggi e cambiano stato, per esempio da in attività a in pausa.
Un coprocesso è simile a un thread, ma passa il controllo a un altro solo quando lo cede lui stesso, per esempio con yield o mettendosi in attesa. Un thread invece viene alternato dal gestore dei thread. Qui si usano i thread.
I thread condividono tutta la memoria, tranne le variabili locali dei metodi. Per scambiarsi messaggi basta quindi una zona di memoria condivisa. Un campo contiene un solo valore, mentre una sequenza di messaggi richiede una coda: chi invia inserisce da un lato, chi riceve estrae dall'altro, e le richieste si servono nell'ordine di arrivo. Ogni ufficio ha la sua coda, in cui i thread delle persone mettono le richieste.
Una coda si realizza con una lista collegata: ogni elemento riferisce il precedente e il successivo, e la lista riferisce il primo e l'ultimo. Inserire ed estrarre agli estremi non richiede di scorrere la lista.
Se però due thread modificano la lista insieme, uno da un lato e uno dall'altro, i riferimenti fra gli elementi possono perdere coerenza: un thread può essere sospeso a metà dell'aggiornamento mentre l'altro agisce. Servirebbe la sincronizzazione, che si vedrà più avanti. Intanto si usa una coda che si sincronizza da sola.
BlockingQueue è un'interfaccia di java.util.concurrent: una coda in cui si inserisce da un lato e si estrae dall'altro, che sincronizza da sola gli accessi dei thread. Essendo un'interfaccia, non si scrive new BlockingQueue: si crea un oggetto di una classe che la implementa. LinkedBlockingQueue la realizza con una lista collegata, altre classi con un array.
add(m) inserisce m in fondo e torna subito: non attende mai.take() toglie e restituisce il primo elemento, il più vecchio. Se la coda è vuota, il thread si ferma finché un altro non inserisce qualcosa.Un thread fermo in take() può essere interrotto con interrupt(), come in una sleep. Per questo take() lancia InterruptedException e va chiamato in un try.
Lo schema in cui un thread produce richieste e un altro le serve si chiama produttore-consumatore ed è comune nei programmi concorrenti. Qui il produttore inserisce un numero casuale da 0 a 100, scritto come stringa, e il consumatore lo stampa. Le pause casuali rallentano i thread, altrimenti l'uscita sarebbe troppo veloce per essere letta.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Principale {
static BlockingQueue<String> messaggi = new LinkedBlockingQueue<String>();
static void sleep(long millisecondi) {
try {
Thread.sleep(millisecondi);
} catch (InterruptedException e) {
System.out.println(e);
}
}
public static void main(String[] args) {
Produttore produttore = new Produttore();
Consumatore consumatore = new Consumatore();
Thread tp = new Thread(produttore);
Thread tc = new Thread(consumatore);
tp.start();
tc.start();
}
}
public class Produttore implements Runnable {
public void run() {
while (true) {
int numero = (int) (101 * Math.random());
Principale.messaggi.add("" + numero);
Principale.sleep((long) (3000 * Math.random()));
}
}
}
public class Consumatore implements Runnable {
public void run() {
while (true) {
try {
String m = Principale.messaggi.take();
System.out.println(m);
} catch (InterruptedException e) {
return;
}
Principale.sleep((long) (5000 * Math.random()));
}
}
}
Se un'attesa del consumatore viene interrotta, non c'è nessun messaggio da stampare: il catch termina il metodo.
Il produttore attende fino a 3 secondi e il consumatore fino a 5, quindi il produttore è più rapido. I messaggi si accumulano nella coda e il consumatore li stampa nello stesso ordine in cui sono stati inseriti. Invertendo i tempi il consumatore trova la coda vuota e aspetta in take(): appena il produttore inserisce un messaggio, lo stampa, e la coda contiene quasi sempre al più un elemento.
Nel client dell'elevatore l'interfaccia è il produttore e un thread gestore è il consumatore. La coda dei comandi si crea all'avvio, prima della finestra, e il programma principale lancia il gestore. Conviene dargli un nome con setName: nelle prove si distingue subito dagli altri thread.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Principale {
static BlockingQueue<String> comandi = new LinkedBlockingQueue<String>();
public static void main(String[] args) {
Gestore gestore = new Gestore();
Thread tg = new Thread(gestore);
tg.setName("gestore");
tg.start();
Finestra finestra = new Finestra();
}
}
L'ascoltatore dei pulsanti inserisce il comando nella coda, secondo il pulsante premuto. Il gestore lo estrae e lo serve.
Chi usa la coda dei comandi
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class Ascoltatore implements ActionListener {
public void actionPerformed(ActionEvent e) {
Principale.comandi.add(e.getActionCommand());
}
}
Il gestore ripete all'infinito la stessa sequenza: estrae un comando, chiede l'altezza al rilevatore e, se l'altezza lo consente, invia il comando al motore e aspetta la risposta. Se la coda è vuota resta fermo in take(), quindi non serve nessun controllo. I due socket si aprono una sola volta, all'avvio del gestore: gli indirizzi dei server sono fissi e restano gli stessi finché il programma gira. Il try racchiude tutto il corpo del ciclo, perché se l'attesa è interrotta il comando non esiste e il resto non si può eseguire.
Conviene provare la struttura prima della rete: si simula il motore, e il gestore stampa il comando ricevuto e attende un tempo casuale. Solo dopo si aggiungono i socket.
public class Gestore implements Runnable {
public void run() {
while (true) {
try {
String comando = Principale.comandi.take();
System.out.println(comando);
Thread.sleep((long) (3000 * Math.random()));
} catch (InterruptedException e) {
return;
}
}
}
}
La coda risolve il problema della sincronizzazione senza che il programma debba occuparsene. Nell'ascoltatore, che gira nel thread dell'interfaccia, si possono mettere solo operazioni che non attendono: add torna subito e va bene lì, mentre take può fermare il thread e blocca l'interfaccia, quindi sta nel gestore.
Oggetti e thread
Coda di messaggi
BlockingQueue
LinkedBlockingQueue:
lista collegataadd(m): inserisce in fondo,
non attendetake(): estrae il primo,
se vuota attendetake() lancia
InterruptedExceptionProduttore-consumatore
Interfaccia grafica
add, mai taketake() nel thread gestore