W3docs

Interfaccia Runnable di Java

Definisci unità di lavoro per i thread in Java con l'interfaccia funzionale Runnable — la forma preferita per thread, executor e virtual thread.

Runnable è un'interfaccia con un solo metodo — probabilmente la più importante in java.lang. Tutto ciò che "viene eseguito su un thread" in Java è, in ultima analisi, un Runnable da qualche parte: il costruttore di Thread ne accetta uno, ExecutorService.execute ne accetta uno, gli shutdown hook della JVM ne accettano uno. Il motivo per cui il capitolo precedente consigliava di "passare un Runnable al costruttore di Thread" anziché "estendere Thread" è che Runnable separa cosa viene eseguito da cosa lo esegue. Questa separazione è ciò che permette alla stessa attività di funzionare su un platform thread, un thread pool o un virtual thread senza modificare il codice.

La struttura

L'intera definizione sta in tre righe:

@FunctionalInterface
public interface Runnable {
  void run();
}

Tutto qui. Da quelle tre righe derivano due conseguenze:

  • È un'interfaccia funzionale. Qualsiasi lambda o riferimento a metodo con firma senza argomenti e void la implementa: () -> System.out.println("hi"), this::flush, Foo::staticMethod.
  • Restituisce void e non lancia eccezioni checked. Questo è il limite di ciò che puoi esprimere. Se hai bisogno di un risultato, o di lanciare qualcosa di checked, hai bisogno di Callable (un capitolo o due più avanti).

Tre modi per scriverne uno

// 1. Lambda — the modern default
Runnable r1 = () -> System.out.println("hello");

// 2. Method reference — when an existing method has the right signature
Runnable r2 = System.out::flush;

// 3. Anonymous class — pre-Java-8 form, occasionally useful when the body needs fields
Runnable r3 = new Runnable() {
  @Override public void run() {
    System.out.println("hello");
  }
};

Tutti e tre producono un oggetto di tipo Runnable. La forma lambda è preferita da Java 8; la forma con classe anonima è utile solo quando hai bisogno di campi propri (il che di solito non accade — cattura invece le variabili locali).

Come viene utilizzato Runnable

Tre delle principali API che accettano Runnable:

new Thread(runnable).start();                 // platform thread, dedicated
executor.execute(runnable);                   // thread pool or virtual thread
Runtime.getRuntime().addShutdownHook(new Thread(runnable));  // JVM shutdown

La stessa istanza Runnable funziona in tutti e tre i contesti. Questo è il punto di progettazione: il cosa (il lavoro) e il dove (il thread) sono ortogonali. Puoi scrivere codice che svolge il lavoro e qualcun altro può decidere su cosa eseguirlo.

Il contrasto con la forma della sottoclasse di Thread rende questo concreto:

// Coupled: this work can only run on its own dedicated platform thread.
class ImageResizer extends Thread {
  @Override public void run() { resize(); }
}
new ImageResizer().start();

// Decoupled: the same body runs anywhere.
Runnable resize = this::resize;
new Thread(resize).start();                  // dedicated thread
executor.execute(resize);                    // pool
virtualExecutor.execute(resize);             // virtual thread

La forma disaccoppiata è il motivo per cui il Java di produzione è pieno di Runnable (e Callable) e quasi mai ha una classe che estende Thread.

Le variabili catturate devono essere effettivamente finali

Una lambda che diventa un Runnable può leggere le variabili locali del metodo che la racchiude, ma solo quelle che il compilatore può dimostrare essere effettivamente finali — assegnate esattamente una volta e mai riassegnate:

String name = "alice";
int n = 3;
Runnable r = () -> {
  for (int i = 0; i < n; i++) {
    System.out.println(name + " " + i);
  }
};
// n = 4;                                   // would break the lambda above — compile error

Se hai bisogno di stato mutabile condiviso, non puoi usare una variabile locale catturata — hai bisogno di un campo, un AtomicInteger, uno slot di array o un altro oggetto i cui interni siano mutabili. La restrizione è intenzionale: le lambda catturano valori, non alias, e vietare la riassegnazione è la regola più semplice che rende questo comportamento coerente.

La soluzione più comune è l'array a un elemento:

int[] counter = {0};
Runnable r = () -> counter[0]++;             // works; the array reference is final, the int inside isn't

Ma per i contatori condivisi thread-safe, un AtomicInteger è la scelta giusta — vedremo perché tra qualche capitolo.

Gestione delle eccezioni: niente da catturare, niente da recuperare

run() non lancia eccezioni checked. Se il tuo worker può fallire con un'eccezione checked, devi catturarla all'interno di run():

Runnable parseFile = () -> {
  try {
    Files.readAllLines(path);
  } catch (IOException e) {
    log.error("parse failed", e);            // you HAVE to handle it here
  }
};

Per le eccezioni unchecked, la situazione è peggiore: niente nel codice chiamante le cattura. Se il tuo Runnable lancia NullPointerException su un thread separato, l'eccezione va al gestore delle eccezioni non catturate di quel thread e il thread muore. Il thread principale non lo sa.

Due modi per affrontare questo problema:

  1. Cattura tutto dentro run() e registralo tu stesso. Grezzo ma affidabile.
  2. Usa Callable e Future.get(). Il Future rilancia l'eccezione sul thread che ha chiamato get(). Questo è ciò che ti offre il framework executor.

Per il lavoro occasionale, l'opzione 1 va bene; per tutto ciò che produce un risultato di cui il chiamante ha bisogno, l'opzione 2 è la risposta giusta.

Runnable vs. Callable

Un confronto fianco a fianco delle due interfacce per i task — incontrerai Callable correttamente più avanti, ma il contrasto è utile ora:

RunnableCallable<V>
Metodovoid run()V call() throws Exception
Valore di ritornoNessunoRisultato tipizzato V
Eccezioni checkedNon può lanciarePuò lanciare qualsiasi Exception
Accettato danew Thread, Executor.execute, shutdown hookExecutorService.submit
Gestore del risultatoNessuno (fire and forget)Future<V>

Ogni volta che hai bisogno sia di un valore di ritorno sia della possibilità di lanciare eccezioni checked, passa a Callable. Per il lavoro puramente con effetti collaterali — flush, logging, scheduling — Runnable è lo strumento più leggero.

Un esempio pratico: stesso Runnable, tre runner

Il programma seguente definisce un Runnable che svolge una piccola operazione, poi esegue la stessa istanza su (a) un nuovo platform thread, (b) un ExecutorService, e (c) il thread chiamante tramite .run() diretto. Lo stesso corpo viene eseguito in tutti e tre i contesti; l'unica cosa che cambia è il runner.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • I primi tre blocchi hanno eseguito la stessa istanza greet in tre runner diversi — chiamata diretta, thread dedicato, thread pool. Il nome del thread stampato da greet cambiava ogni volta: main, dedicated-worker, pool-1-thread-1. Questo è il motivo principale per preferire Runnable rispetto alla sottoclasse di Thread: il lavoro è riutilizzabile, il runner è intercambiabile.
  • L'eccezione RuntimeException del thread crashy non ha ucciso main. È morta sul suo thread e il gestore delle eccezioni non catturate l'ha segnalata. Senza un gestore, la JVM stampa uno stack trace su stderr e il resto del programma continua a girare — il che è spesso peggio, perché il lavoro che il thread avrebbe dovuto svolgere silenziosamente non è avvenuto.
  • La lambda shout ha catturato name e n dalle variabili locali di main. Sono effettivamente finali — assegnate una volta, mai riassegnate. Aggiungi n = 4; in qualsiasi punto dopo la definizione della lambda e il file smette di compilarsi. Questa restrizione è ciò che rende la cattura nelle lambda sicura tra i thread.
  • L'esempio bump ha usato AtomicInteger perché due thread incrementavano lo stesso contatore. Con un campo int semplice, il valore finale sarebbe stato tra 1000 e 2000 — aggiornamenti persi a causa di i++ non atomico. incrementAndGet() è la correzione più semplice e torneremo su di essa nel capitolo sugli atomics.
  • La singola istanza condivisa Runnable è stata passata a new Thread(bump, "a") e new Thread(bump, "b") — la stessa lambda era in esecuzione su due thread simultaneamente. La lambda non ha campi propri; tutto ciò che tocca vive al di fuori di essa. Questa è la forma di ogni Runnable parallelo sicuro: il meno stato interno possibile, lo stato viene portato in un oggetto thread-safe condiviso dai thread.

Cosa c'è dopo

Il prossimo capitolo, Ciclo di vita del thread Java, illustra i sei valori di Thread.StateNEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — e mostra come leggere un thread dump che li espone.

Esercizio

Pratica
Quale affermazione su `Runnable` è vera?
Quale affermazione su `Runnable` è vera?
Was this page helpful?