Lezione 15 · Canale 2 · mercoledì 28 ottobre 2026
Introduzione ai thread in Java
Progettazione del Software
Lezione 15 · Canale 2 · mercoledì 28 ottobre 2026
Progettazione del Software
Un processore esegue un'istruzione alla volta, ma in tempi brevissimi. Il sistema operativo e la macchina virtuale danno il processore a turno ai flussi da eseguire: mentre uno aspetta l'utente o il disco, il processore lavora per un altro. I turni sono così brevi che i flussi sembrano procedere insieme. Questa alternanza si chiama interleaving. Con più core, flussi diversi girano davvero insieme; quando i flussi sono più dei core si alternano comunque. Quando avviene il passaggio non lo decide il programma.
Un thread è un flusso di esecuzione sequenziale e indipendente dentro un programma, detto anche lightweight process. Ogni thread ha solo ciò che serve a un flusso indipendente: il suo contesto e il suo stack. Lo spazio di indirizzamento, compreso lo heap, è comune a tutti i thread del processo. Per questo creare un thread è più rapido che creare un processo.
Anche un programma Swing ha più thread: il main costruisce la finestra e termina, ma eventi e disegno li gestisce un altro thread, che continua.
I thread servono a simulare attività simultanee, a usare più processori per lo stesso programma, a sfruttare bene le risorse lente e a tenere reattiva l'interfaccia mentre un'operazione lunga gira in un thread dedicato. Il prezzo è la complessità: attività concorrenti possono interferire, soprattutto sui dati condivisi.
Il lavoro e chi lo esegue sono due oggetti distinti:
Runnable, e il lavoro sta nel suo metodo public void run();Thread, che riceve l'eseguibile nel costruttore e lo esegue in un thread nuovo.Thread e Runnable stanno in java.lang, che non si importa. Un oggetto Thread non è il thread: è lo strumento con cui si chiede alla macchina virtuale di crearne uno. Creare l'oggetto non avvia niente.
public class MioRunnable implements Runnable {
private String nome;
public MioRunnable(String n) {
nome = n;
}
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(nome + ": " + i);
}
System.out.println(nome + ": DONE! ");
}
}
public class Main {
public static void main(String[] args) {
MioRunnable ja = new MioRunnable("Jamaica");
Thread mioThread = new Thread(ja);
mioThread.start();
System.out.println("main: fine");
}
}
Lo schema è sempre di tre istruzioni: si crea l'eseguibile, si crea l'esecutore passandogli l'eseguibile, si chiama start() sull'esecutore. Prima di start() c'è un solo flusso; da lì in poi due.
Esecutore ed eseguibile
start() crea il thread, vi esegue run() e ritorna subito. Chiamare direttamente ja.run() è invece una chiamata come le altre: run() gira nel thread del chiamante, e nessun thread nuovo parte.
Di solito il programma stampa prima main: fine e poi il conteggio di Jamaica: dopo start() il main ha ancora il processore. L'ordine però non è garantito. Se il main dorme un po' prima di stampare, cede il processore e la sua riga esce per ultima.
Si potrebbe anche estendere Thread e ridefinire run(). Il risultato è lo stesso, ma la classe non può più estendere altre classi, perché l'ereditarietà è singola, e mescola il lavoro con il meccanismo che lo esegue. Si usa sempre Runnable più Thread.
Due thread stampano cento volte un carattere ciascuno, uno 0 e l'altro 1:
public class PrintExecutable implements Runnable {
private final char c;
public PrintExecutable(char c) {
this.c = c;
}
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(c);
}
}
}
Thread t1 = new Thread(new PrintExecutable('0'));
Thread t2 = new Thread(new PrintExecutable('1'));
t1.start();
t2.start();
Spesso escono cento 0 e poi cento 1. I thread girano comunque in modo concorrente: il primo è solo così veloce da finire prima che il secondo ottenga il turno.
Thread.sleep(millisec) sospende per quei millisecondi il thread che la esegue: il thread diventa non eseguibile e, scaduto il tempo, torna pronto, ma non riparte per forza subito. Non serve a rispettare scadenze precise; due secondi sono 2000. Può lanciare InterruptedException, da gestire con try e catch o da rilanciare.
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(c);
try {
Thread.sleep((long) (Math.random() * 10));
} catch (InterruptedException e) {
}
}
}
Dopo ogni carattere il thread cede il processore per qualche millisecondo, e gli 0 e gli 1 si alternano in modo diverso a ogni esecuzione.
static è comune a tutti i thread del programma.Thread.currentThread() restituisce il Thread che sta eseguendo l'istruzione, e getName() il suo nome, che si assegna con setName:
public class RunExecutable implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " is running " + i);
}
}
}
Thread alpha = new Thread(new RunExecutable());
Thread beta = new Thread(new RunExecutable());
alpha.setName("Alpha thread");
beta.setName("Beta thread");
alpha.start();
beta.start();
I due conteggi si intercalano, ma ciascuno va in ordine da 0 a 99: i è una variabile del metodo, e ogni thread ha la sua.
Se i diventa un campo, conta come è dichiarato. Con private int i ogni thread ha il suo oggetto RunExecutable, quindi il suo campo, e non cambia nulla: private regola l'accesso, non la condivisione. Con private static int i il campo è uno solo e i due thread lo incrementano entrambi. Allora l'uscita perde l'ordine: un thread aumenta i e, prima di stamparlo, perde il turno; l'altro lo aumenta ancora. Alcuni valori non li stampa nessuno, insieme i due thread fanno circa 100 giri invece di 200, e ogni esecuzione dà un risultato diverso.
Un dato modificato da più thread senza coordinamento fa perdere il controllo del programma. Servono meccanismi di sincronizzazione che lascino accedere un thread alla volta, a costo di farli aspettare: in Java sono i metodi synchronized.
Thread
Runnable
con run()new Thread(r)t.start(): run() in
un thread nuovor.run(): nello stesso
threadOrdine
Thread.sleep(ms):
sospende il thread
correnteInterruptedExceptionMemoria
Variabili
static: comuni a tutti