W3docs

Metodi dei Thread in Java

Metodi di controllo dei thread Java — start, run, sleep, join, interrupt, yield, setDaemon — con le insidie che colpiscono il codice reale.

Thread ha molti metodi, ma solo una manciata sono quelli che chiamerai nel codice reale. Questo capitolo esamina quella manciata — cosa fa ciascuno, cosa non fa, e gli errori che sembrano corretti finché non lo sono. I capitoli precedenti hanno introdotto questi metodi di passaggio; qui vengono analizzati individualmente.

Questa pagina tratta il ciclo di vita e il controllo dei thread: start vs run, sleep, join, il protocollo di cancellazione cooperativa interrupt, più i piccoli helper (yield, currentThread, naming, stato daemon, priorità, holdsLock, onSpinWait). Per il quadro generale di come un thread si muove attraverso NEW, RUNNABLE, BLOCKED, WAITING e TERMINATED, vedi Java Thread Life Cycle.

start() vs. run()

Il bug di multithreading più comune:

Thread t = new Thread(() -> work(), "worker");
t.run();                                      // wrong: runs work() on the CURRENT thread
t.start();                                    // right: spawns a new OS thread, returns immediately

start() è l'unico metodo che crea un nuovo thread del sistema operativo. run() è il corpo del lavoro — chiamarlo direttamente è semplicemente una normale invocazione di metodo che ritorna quando il lavoro finisce. Se non vedi start(), non sta avvenendo alcun parallelismo.

start() è anche monouso. Dopo che run() ritorna, il thread è TERMINATED e non può essere riavviato. Una seconda chiamata a start() lancia IllegalThreadStateException.

Thread.sleep(ms)

La chiamata statica che sospende il thread corrente per almeno la durata specificata:

Thread.sleep(1500);                          // sleep 1.5 seconds
Thread.sleep(0, 250);                        // 250 nanoseconds; precision varies by OS
Thread.sleep(Duration.ofMillis(1500));       // Java 19+ overload

Tre cose da sapere:

  • Lancia InterruptedException. Il sleep è interrompibile — è così che viene detto a un worker di smettere di dormire e spegnersi. Puoi propagare l'eccezione (dichiarare throws) oppure catturarla e riattivare il flag con Thread.currentThread().interrupt().
  • Non rilascia i lock. Un thread in sleep mantiene tutti i lock che aveva prima. Se usi Thread.sleep all'interno di un blocco synchronized, nessun altro thread entra nel blocco mentre dormi. Quasi sempre è un bug; usa wait o Condition.await (vedi Inter-thread Communication) quando hai bisogno di rilasciare il lock.
  • Il timing è "almeno", non "esattamente". Il sistema operativo potrebbe svegliarti un tick in ritardo sotto carico; non ti sveglia mai un tick in anticipo.

t.join() e t.join(ms)

Aspetta che un altro thread finisca:

t.join();                                    // block until t terminates
t.join(2000);                                // block up to 2 seconds, then continue regardless
boolean done = t.join(Duration.ofSeconds(2));// Java 19+, returns whether it finished

join è il modo in cui componi lavoro parallelo multi-step: avvii alcuni thread, li lasci girare, li join tutti, leggi i loro risultati. join() ritorna quando il run() del thread target è tornato (normalmente o per eccezione). Lancia anche InterruptedException così i chiamanti possono essere interrotti dall'attesa.

Una sottigliezza: join(0) significa "join senza timeout" (cioè aspetta per sempre), non "join con timeout zero". Se vuoi un vero "abbandona immediatamente", usa invece t.isAlive().

t.interrupt() e il flag

Il protocollo di cancellazione cooperativa, in tre chiamate:

t.interrupt();                               // set t's interrupt flag (and unblock sleep/wait/join/park)
t.isInterrupted();                           // ask whether the flag is set (does NOT clear)
Thread.interrupted();                        // static; ask current thread, and CLEAR the flag

Il flag è semplicemente un volatile boolean sull'oggetto Thread. interrupt() lo imposta. Se t si trova attualmente in sleep, wait, join o LockSupport.park (o molte chiamate bloccanti java.nio), quella chiamata bloccante lancia immediatamente InterruptedException. Altrimenti il flag aspetta che il worker lo noti autonomamente.

Un worker che vuole essere interrompibile ha due responsabilità:

  1. Controllare Thread.currentThread().isInterrupted() tra i passi di lunga durata.
  2. In ogni catch (InterruptedException e), propagare o reimpostare il flag con Thread.currentThread().interrupt() — non ingoiarlo mai silenziosamente.
while (!Thread.currentThread().isInterrupted()) {
  try {
    doOneUnit();
    Thread.sleep(100);
  } catch (InterruptedException e) {
    Thread.currentThread().interrupt();        // restore flag for the loop check
  }
}

Thread.yield() — quasi mai lo strumento giusto

Thread.yield();                              // hint: please run someone else

Un suggerimento non vincolante allo scheduler. Il sistema operativo è libero di ignorarlo. Non esiste praticamente alcun codice in produzione che abbia bisogno di yield — se vuoi aspettare un evento, usa wait, una Condition o un Semaphore. Ricorri a yield solo per micro-benchmark, harness di test per deadlock, o quando stai scrivendo la JVM stessa.

Thread.currentThread()

L'accessor statico per il thread su cui si trova il codice chiamante. I due utilizzi che vedrai:

String who = Thread.currentThread().getName();
Thread.currentThread().interrupt();          // re-arm the flag after catching InterruptedException

getName() è anche il modo standard per etichettare le righe di log così puoi distinguere i thread nell'output in produzione.

getName / setName

I nomi contano per il debugging. Il nome predefinito (Thread-3) è inutile in un thread dump.

Thread t = new Thread(this::flush, "flush-loop");      // name at construction (preferred)
t.setName("flush-loop-2");                              // rename later if a role changes

Puoi rinominare in qualsiasi momento, ma il valore al momento del dump o del log è quello che il lettore vedrà. Passa sempre un nome al costruttore.

setDaemon(true)

Thread t = new Thread(this::poll, "metrics-poller");
t.setDaemon(true);                           // BEFORE start(); else IllegalThreadStateException
t.start();

I thread daemon non mantengono la JVM in vita — quando l'ultimo thread non-daemon esce, la JVM rimuove brutalmente i daemon. Usali per operazioni di manutenzione che devono terminare con il programma (timer, flusher di metriche, polling loop). Non usarli per lavori il cui completamento ti serve davvero.

setPriority(int)

t.setPriority(Thread.MAX_PRIORITY);          // 10
t.setPriority(Thread.MIN_PRIORITY);          // 1
t.setPriority(Thread.NORM_PRIORITY);         // 5 (default)

Per lo più consultivo. Il prossimo capitolo tratta le priorità in dettaglio; per ora, il titolo: non fare affidamento su di esse per la correttezza, il sistema operativo decide cosa significano.

Thread.holdsLock(obj)

Un helper statico di debug:

assert Thread.holdsLock(monitor) : "expected to be inside a synchronized block on monitor";

Ritorna true se il thread chiamante detiene il monitor intrinseco di obj. Utile per affermare "questo metodo deve essere chiamato solo dall'interno di un blocco synchronized" senza pagare il costo di acquisizione del lock nel percorso felice.

Thread.onSpinWait() — Java 9+

while (!done) {
  Thread.onSpinWait();                       // hint to the CPU: I'm spinning, slow down
}

Un suggerimento a livello CPU che mette in pausa le pipeline e riduce il consumo energetico durante un tight spin loop. È specificamente per il caso molto ristretto in cui stai girando per pochi microsecondi aspettando che un altro thread inverta un flag; non è una chiamata generica "cedi la CPU". Per qualsiasi cosa più lunga, usa LockSupport.park o una Condition.

Un esempio pratico: la maggior parte di questi in un unico posto

Il programma seguente usa start, join con un timeout, interrupt, sleep, isInterrupted e setName insieme — i metodi che chiameresti effettivamente in produzione.

java— editable, runs on the server

Cosa dedurre dall'esecuzione:

  • La riga bad.run() ha stampato ran on: main. Nessun nuovo thread è stato creato. bad.isAlive() era false in seguito perché start() non è mai stato chiamato. Ogni programma di multithreading ad un certo punto ha questo bug; una volta che lo hai commesso, non lo commetti mai più.
  • slow.join(300) è ritornato dopo circa 300 ms anche se slow avrebbe dormito per 2000. isAlive() era ancora true. join(ms) è l'attesa limitata — utile quando vuoi dare a un worker una possibilità elegante di finire prima di escalare.
  • slow.interrupt() ha terminato immediatamente il suo Thread.sleep lanciando InterruptedException all'interno del worker. Questo è il contratto: le chiamate bloccanti interrompibili reagiscono a interrupt() uscendo con l'eccezione, che è come funziona la cancellazione cooperativa in pratica.
  • Il worker bookkeeper ha catturato InterruptedException e ha riattivato il flag con Thread.currentThread().interrupt(). Il successivo isInterrupted() ha restituito true. Senza quella riattivazione, il flag viene perso e qualsiasi codice più in alto nello stack delle chiamate pensa che non sia mai avvenuto alcun interrupt.
  • daemon.setDaemon(true) è stato chiamato prima di start() — chiamarlo dopo avrebbe lanciato IllegalThreadStateException. E quando main è tornato, il daemon è stato ucciso a metà del sleep; la JVM è uscita perché non rimaneva alcun thread non-daemon. Questo è il compromesso del daemon: non blocca mai l'uscita della JVM, non è garantito che finisca.

Cosa c'è dopo

Il prossimo capitolo, Java Thread Priority, tratta il metodo setPriority su Thread, cosa fanno effettivamente le priorità sui sistemi operativi reali, e perché dovresti trattarle come un suggerimento piuttosto che una garanzia.

Pratica

Pratica
Catturi `InterruptedException` in un worker ma non vuoi uscire dal loop. Cosa dovresti fare con il flag di interrupt?
Catturi `InterruptedException` in un worker ma non vuoi uscire dal loop. Cosa dovresti fare con il flag di interrupt?
Was this page helpful?