W3docs

L'interface Runnable en Java

Définissez des unités de travail pour les threads en Java avec l'interface fonctionnelle Runnable — la forme privilégiée pour les threads et les executors.

Runnable est une interface à une seule méthode — probablement la plus importante de java.lang. Tout ce qui « s'exécute sur un thread » en Java est, in fine, un Runnable quelque part : le constructeur de Thread en accepte un, ExecutorService.execute en accepte un, les shutdown hooks de la JVM en acceptent un. Si le chapitre précédent recommandait « passer un Runnable au constructeur de Thread » plutôt que « étendre Thread », c'est parce que Runnable sépare ce qui s'exécute de ce qui l'exécute. Cette séparation permet à la même tâche de fonctionner sur un thread de plateforme, un pool de threads ou un thread virtuel sans modifier le code.

La forme

Toute la définition tient en trois lignes :

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

C'est tout. Deux conséquences découlent de ces trois lignes :

  • C'est une interface fonctionnelle. Tout lambda ou référence de méthode avec une signature sans argument et void l'implémente : () -> System.out.println("hi"), this::flush, Foo::staticMethod.
  • Elle retourne void et ne lance pas d'exceptions vérifiées. C'est la limite de ce que vous pouvez exprimer. Si vous avez besoin d'un résultat, ou de lancer une exception vérifiée, vous avez besoin de Callable (un ou deux chapitres plus loin).

Trois façons d'en écrire un

// 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");
  }
};

Les trois produisent un objet de type Runnable. La forme lambda est préférée depuis Java 8 ; la forme avec classe anonyme n'est utile que lorsque vous avez besoin de vos propres champs (ce qui est rarement le cas — capturez plutôt des variables locales).

Comment Runnable est utilisé

Trois des principales API qui acceptent un 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 même instance de Runnable fonctionne dans les trois contextes. C'est le point de conception : le quoi (le travail) et le (le thread) sont orthogonaux. Vous pouvez écrire du code qui effectue le travail et laisser quelqu'un d'autre décider sur quoi l'exécuter.

Le contraste avec la forme de sous-classe de Thread rend cela concret :

// 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 forme découplée explique pourquoi le Java de production est plein de Runnable (et de Callable) et ne comporte presque jamais de classe qui étend Thread.

Les variables capturées doivent être effectivement finales

Un lambda qui devient un Runnable peut lire les variables locales de la méthode englobante, mais uniquement celles que le compilateur peut prouver comme effectivement finales — assignées exactement une fois et jamais réassignées :

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

Si vous avez besoin d'un état mutable partagé, vous ne pouvez pas utiliser une variable locale capturée — vous avez besoin d'un champ, d'un AtomicInteger, d'un slot de tableau, ou d'un autre objet dont les éléments internes sont mutables. La restriction est intentionnelle : les lambdas capturent des valeurs, pas des alias, et interdire la réassignation est la règle la plus simple pour rendre cela cohérent.

La solution la plus courante est le tableau à un élément :

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

Mais pour des compteurs partagés et thread-safe, un AtomicInteger est le bon choix — nous verrons pourquoi dans quelques chapitres.

Gestion des exceptions : rien à attraper, rien à récupérer

run() ne lance pas d'exceptions vérifiées. Si votre tâche peut échouer avec une exception vérifiée, vous devez la capturer à l'intérieur de run() :

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

Pour les exceptions non vérifiées, la situation est pire : rien dans le code appelant ne les capture. Si votre Runnable lance une NullPointerException sur un thread séparé, l'exception est transmise au gestionnaire d'exceptions non capturées de ce thread et le thread meurt. Le thread principal n'en sait rien.

Deux façons de gérer cela :

  1. Capturer tout à l'intérieur de run() et le journaliser vous-même. Rudimentaire mais fiable.
  2. Utiliser Callable et Future.get(). Le Future relance l'exception sur le thread qui a appelé get(). C'est ce que le framework d'executors vous offre.

Pour du travail ponctuel, l'option 1 convient ; pour tout ce qui produit un résultat dont l'appelant a besoin, l'option 2 est la bonne réponse.

Runnable vs. Callable

Une comparaison côte à côte des deux interfaces de tâche — vous rencontrerez Callable correctement plus tard, mais le contraste est utile maintenant :

RunnableCallable<V>
Méthodevoid run()V call() throws Exception
Valeur de retourAucuneRésultat typé V
Exceptions vérifiéesNe peut pas lancerPeut lancer n'importe quelle Exception
Accepté parnew Thread, Executor.execute, shutdown hooksExecutorService.submit
Gestionnaire de résultatAucun (fire and forget)Future<V>

Dès que vous avez besoin soit d'une valeur de retour soit de la capacité à lancer des exceptions vérifiées, passez à Callable. Pour du travail purement à effets de bord — flush, journalisation, planification — Runnable est l'outil le plus léger.

Un exemple concret : le même Runnable, trois runners

Le programme ci-dessous définit un Runnable qui effectue un petit travail, puis exécute la même instance sur (a) un tout nouveau thread de plateforme, (b) un ExecutorService, et (c) le thread appelant via un appel direct à .run(). Le même corps s'exécute dans les trois contextes ; seul le runner change.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • Les trois premiers blocs ont exécuté la même instance greet dans trois runners différents — appel direct, thread dédié, pool de threads. Le nom du thread affiché par greet a changé à chaque fois : main, dedicated-worker, pool-1-thread-1. C'est la raison principale de préférer Runnable à la sous-classe de Thread : le travail est réutilisable, le runner est interchangeable.
  • La RuntimeException du thread crashy n'a pas tué main. Elle est morte sur son propre thread et le gestionnaire d'exceptions non capturées l'a signalée. Sans gestionnaire, la JVM imprime une trace de pile vers stderr et le reste du programme continue — ce qui est souvent pire, car le travail que le thread était censé faire n'a silencieusement pas eu lieu.
  • Le lambda shout a capturé name et n depuis les variables locales de main. Elles sont effectivement finales — assignées une fois, jamais réassignées. Ajoutez n = 4; n'importe où après la définition du lambda et le fichier ne compilera plus. Cette restriction est ce qui rend la capture dans les lambdas sûre à travers les threads.
  • L'exemple bump a utilisé AtomicInteger parce que deux threads incrémentaient le même compteur. Avec un champ int ordinaire, la valeur finale aurait été quelque part entre 1000 et 2000 — des mises à jour perdues dues à un i++ non atomique. incrementAndGet() est la correction la plus simple et nous y reviendrons dans le chapitre sur les atomics.
  • La même instance de Runnable partagée a été passée à new Thread(bump, "a") et new Thread(bump, "b") — le même lambda s'est exécuté sur deux threads simultanément. Le lambda n'a pas de champs propres ; tout ce qu'il touche vit à l'extérieur. C'est la forme de tout Runnable parallèle sûr : avoir le moins possible d'état interne, et pousser l'état dans un objet thread-safe partagé par les threads.

La suite

Le prochain chapitre, Cycle de vie d'un Thread Java, parcourt les six valeurs de Thread.StateNEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — et montre comment lire un thread dump qui les expose.

Pratique

Pratique
Quelle affirmation sur `Runnable` est vraie ?
Quelle affirmation sur `Runnable` est vraie ?
Was this page helpful?