W3docs

La classe Thread in Java

Crea e controlla thread in Java estendendo Thread o passando un Runnable, con i compromessi tra i due approcci.

java.lang.Thread è l'oggetto che si tiene come riferimento quando si vuole avviare, nominare, unire, interrompere o interrogare un thread di esecuzione. Il capitolo precedente ha introdotto i thread a livello concettuale; questo è il tour delle API. Tutto in java.util.concurrent — executor, future, thread virtuali — è costruito sopra Thread, quindi vale la pena conoscere la classe grezza anche se nella maggior parte dei casi si preferirà utilizzare i wrapper di livello superiore nel codice di produzione.

Due modi per creare un thread

Un Thread è un Runnable avvolto in un oggetto di controllo. Ci sono due modi per fornirgli il Runnable:

// 1. Pass a Runnable to the constructor (the modern, preferred form)
Thread a = new Thread(() -> System.out.println("hello from " + Thread.currentThread().getName()));

// 2. Extend Thread and override run()
class HelloThread extends Thread {
  @Override public void run() {
    System.out.println("hello from " + getName());
  }
}
Thread b = new HelloThread();

Entrambi funzionano; entrambi eseguono il codice su un thread nuovo. La prima forma è quella usata da quasi tutto il codice moderno, per tre motivi:

  • Una classe può estendere solo un'altra classe. Se si estende Thread, non si può estendere nient'altro — e la parte del codice che è il lavoro raramente ha una buona ragione per essere un thread in senso OO. Passare un Runnable mantiene libera la classe di business.
  • I lambda trasformano la forma Runnable in una riga sola. Creare una sottoclasse di Thread richiede una classe nominata per lo stesso codice.
  • Il Runnable passato può essere consegnato anche a un ExecutorService in seguito. La sottoclasse Thread è vincolata a girare sul proprio thread dedicato.

Si estenda Thread solo quando si vuole davvero aggiungere stato o metodi al thread stesso (raro). Per tutto il resto, si passi un Runnable.

Avvio e attesa

I due metodi che si useranno su quasi ogni thread:

Thread t = new Thread(() -> doWork(), "worker");
t.start();                                    // schedule it; return immediately
t.join();                                     // block the caller until the thread finishes

Alcuni errori comuni per i principianti:

  • start() è ciò che crea il thread OS. Chiamare run() direttamente esegue il corpo sul thread corrente, in modo sincrono — nessun nuovo thread viene avviato. Questo è l'errore di multithreading più comune per i principianti. Se non si vede start(), non è avvenuto alcun parallelismo.
  • start() può essere chiamato solo una volta. Un Thread è monouso. Chiamare start() una seconda volta lancia IllegalThreadStateException. Per eseguire di nuovo lo stesso task, si crei un nuovo Thread o si usi un ExecutorService.
  • join() può lanciare InterruptedException. È una chiamata bloccante. Se qualcuno chiama interrupt() sul thread che è in attesa in join(), l'attesa termina con l'eccezione. Occorre gestirla o propagarla.

join(millis) attende al massimo quel numero di millisecondi prima di tornare, indipendentemente dal fatto che il thread sia terminato o meno. Si usa quando si vuole dare a un worker una possibilità limitata di terminare in modo ordinato prima di procedere.

I costruttori che contano

Thread ha molti costruttori; nella pratica ne contano quattro:

CostruttoreQuando usarlo
new Thread(Runnable)Il caso base. Worker anonimo.
new Thread(Runnable, String name)Quasi sempre preferibile — i nomi appaiono nei log, nei profiler, nei thread dump.
new Thread(ThreadGroup, Runnable, String)Quando si ha bisogno di un gruppo esplicito (raro; i gruppi sono in gran parte deprecati).
new Thread(ThreadGroup, Runnable, String, long stackSize)Quando lo stack di default (circa 1 MB) non è corretto — es. ricorsione profonda o pressione di memoria.

Il costruttore vuoto new Thread() esiste ed esegue un run() vuoto, che non fa nulla. Non c'è motivo di usarlo.

Si nominino sempre i thread. "worker-1", "http-3", "flush-loop" — qualunque sia il ruolo. Un thread dump pieno di Thread-7, Thread-12, Thread-19 è un thread dump illeggibile.

Proprietà di un'istanza Thread

I pochi campi e getter che si toccheranno realmente:

t.setName("scanner-2");                       // any time before or after start()
String name = t.getName();

t.setDaemon(true);                            // BEFORE start(); else IllegalThreadStateException
boolean d = t.isDaemon();

t.setPriority(Thread.NORM_PRIORITY);          // 1..10; mostly advisory, see chapter 6
int p = t.getPriority();

Thread.State s = t.getState();                // NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
boolean alive = t.isAlive();                  // true between start() and run() returning
long id = t.threadId();                       // Java 19+; old name: getId()

Due di questi sono i più importanti:

  • setDaemon(true) decide se il thread mantiene la JVM in vita. Vedere il capitolo precedente — i daemon muoiono con il programma; i non-daemon lo mantengono in esecuzione finché non ritornano.
  • getState() è ciò che si guarda in un thread dump per diagnosticare "perché il thread è bloccato." BLOCKED significa che è in attesa di un lock intrinseco; WAITING/TIMED_WAITING significa che è parcheggiato in wait(), join(), sleep(), LockSupport.park(), ecc.

Metodi statici di Thread

Alcuni metodi statici che si chiamano dall'interno del worker:

Thread.currentThread();                       // the thread that's executing this code
Thread.sleep(2000);                           // pause this thread for ~2000 ms
Thread.yield();                               // hint to the scheduler "go ahead and run someone else"
Thread.interrupted();                         // returns and CLEARS the interrupt flag of currentThread

Thread.sleep è il più comune; lancia InterruptedException, quindi i chiamanti devono gestirla o propagarla. Thread.yield è quasi mai lo strumento giusto — è un suggerimento vago che JVM e OS possono ignorare. Se si vuole coordinare, si usi una vera primitiva di sincronizzazione.

Thread.interrupted() restituisce true se il thread corrente è stato interrotto, e azzera il flag. t.isInterrupted() (metodo di istanza, su un thread diverso) restituisce il flag senza azzerarlo. Confonderli è una fonte comune di interrupt persi.

Interruzione: come si chiede a un thread di fermarsi

Non esiste un t.stop() sicuro (il metodo esiste, ma è deprecato dal 1.1 perché lascia i lock acquisiti e lo stato corrotto). Il protocollo cooperativo di arresto è:

Thread worker = new Thread(() -> {
  while (!Thread.currentThread().isInterrupted()) {
    doOneUnitOfWork();
  }
}, "worker");
worker.start();
// ... later, from somewhere else:
worker.interrupt();
worker.join();

interrupt() imposta il flag di interrupt del worker. Il worker deve verificare il flag in punti sicuri e uscire. Se il worker è bloccato in sleep, wait, join, o in molte chiamate java.nio, la chiamata bloccante lancia immediatamente InterruptedException in modo che il thread possa reagire.

Se si intercetta InterruptedException e non si vuole propagarla, la convenzione è di reimpostare il flag affinché i chiamanti più in alto nello stack vedano ancora l'interrupt:

try { Thread.sleep(1000); }
catch (InterruptedException e) {
  Thread.currentThread().interrupt();         // re-arm the flag
  return;                                     // and give up cooperatively
}

Inghiottire un interrupt senza reimpostare il flag è un bug. Il flag è il modo in cui il resto del programma sa che è stato chiesto di fermarsi.

Un esempio completo: l'intero ciclo di vita in un programma

Il programma seguente crea due worker in modi diversi (Runnable, sottoclasse), osserva le transizioni di stato, li unisce e dimostra il protocollo di interrupt su un terzo worker.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Le transizioni di stato corrispondono al contratto. Prima di start(), entrambi i thread erano NEW. Dopo start(), erano RUNNABLE (o TERMINATED se il lavoro era piccolo e terminava prima della stampa). Dopo join(), entrambi erano TERMINATED. Questo è il ciclo di vita descritto da Thread.State.
  • La riga "t3 ran on thread: main" è il bug da ricordare per sempre. t3.run() ha eseguito il corpo — sul thread chiamante, in modo sincrono. Nessun nuovo thread è stato creato. t3.isAlive() era false in seguito perché start() non è mai stato chiamato. Se si sta debuggando "niente sembra girare in parallelo," verificare se si è scritto start() o run().
  • Il loop di interrupt non ha usato Thread.sleep come attesa principale — ha semplicemente verificato il flag in modo continuativo, con un breve sleep occasionale affinché l'interrupt potesse terminare il sleep prima del tempo. Il contratto è lo stesso in entrambi i casi: isInterrupted() è ciò che il worker controlla; interrupt() è ciò che il richiedente chiama.
  • Il reimpostare il flag all'interno del catch (la riga Thread.currentThread().interrupt()) ha preservato il segnale per qualsiasi codice più in alto nello stack. Senza quella riga, un interrupt catturato e ignorato sparirebbe — che è uno dei modi più semplici per scrivere un thread che non si spegne in modo pulito.
  • Il daemon alla fine stava per dormire per 60 secondi; invece la JVM è uscita non appena main è tornato, uccidendolo nel mezzo del sleep. I thread daemon possono tenere qualsiasi tipo di risorsa — ma possono anche essere interrotti in qualsiasi momento, ecco perché non si dovrebbe mettere del lavoro che richiede commit su di essi.

Cosa c'è dopo

Il prossimo capitolo, Interfaccia Runnable in Java, approfondisce Runnable stesso — cos'è davvero, perché Callable e Future sono stati aggiunti sopra di esso, e come i lambda hanno cambiato l'ergonomia del passaggio di lavoro a un thread.

Pratica

Pratica
Si chiama `t.run()` (non `t.start()`) su un `Thread` il cui `Runnable` stampa il nome del thread corrente. Cosa stampa?
Si chiama `t.run()` (non `t.start()`) su un `Thread` il cui `Runnable` stampa il nome del thread corrente. Cosa stampa?
Was this page helpful?