Priorité des threads Java
Les priorités de threads Java sont une indication, pas une garantie — ce que setPriority fait réellement et quand l'utiliser.
Thread porte une priorité — un entier de 1 à 10. La JVM la transmet à l'ordonnanceur du système d'exploitation comme une indication sur le thread à exécuter lorsque le CPU doit choisir. Cette indication n'est que cela — une indication. Le système d'exploitation est libre de l'ignorer, et sur la plupart des systèmes d'exploitation de bureau et de serveur, l'effet est entre subtil et invisible. Ce chapitre explique à quoi ressemble l'API, ce qui se passe réellement en dessous, et les cas très rares où définir une priorité vaut la peine.
L'API
public final void setPriority(int newPriority);
public final int getPriority();
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;Trois constantes pour la lisibilité — la plupart du code qui touche à la priorité utilise celles-ci plutôt que des entiers bruts :
Thread t = new Thread(this::housekeeping, "gc-poker");
t.setPriority(Thread.MIN_PRIORITY); // 1 — "background"
t.start();
Thread h = new Thread(this::handleRequest, "http");
h.setPriority(Thread.NORM_PRIORITY); // 5 — default
h.start();Le constructeur utilise la priorité du thread parent et la priorité maximale du groupe de threads du parent — généralement 5. La définir à une valeur en dehors de 1..10 lève une IllegalArgumentException.
Ce que « priorité » signifie pour la JVM
Deux choses, dont aucune n'est « ce thread s'exécutera en premier » :
- Une indication d'ordonnancement natif. La JVM traduit le nombre 1–10 en quelque chose que le système d'exploitation hôte comprend. Sur Linux, c'est une valeur
niceviapthread_setschedparam; sur Windows, c'est une constanteTHREAD_PRIORITY_*. La correspondance est définie par l'implémentation de la JVM. - Un plafond maximum par groupe de threads. Un thread ne peut pas être défini à une priorité supérieure à la
maxPriorityde son groupe de threads. Les groupes de threads sont majoritairement dépréciés, mais le plafond s'applique toujours.
Ce que la priorité ne signifie pas :
- Ce n'est pas un « verrou » — un thread à haute priorité ne peut pas préempter un thread à l'intérieur d'une section critique.
- Ce n'est pas une garantie d'ordre. Deux threads à la priorité 10 s'exécutent toujours en concurrence ; l'un d'eux obtient le CPU en premier.
- Cela n'affecte pas l'exactitude. Tout programme qui « ne fonctionne que » grâce aux priorités a un vrai bug de synchronisation caché derrière.
Ce que le système d'exploitation fait réellement
Les différents systèmes d'exploitation hôtes traitent l'indication différemment. Le comportement actuel, en gros :
- Linux. La configuration JVM par défaut traite les priorités Java comme des valeurs
nice.niceest une indication à l'ordonnanceur CFS concernant le partage du CPU. Les utilisateurs non-root ne peuvent qu'abaisser la priorité (nicepositif) ; la relever (nicenégatif) nécessiteCAP_SYS_NICE, que la plupart des JVM n'ont pas. En pratique,setPriority(MAX_PRIORITY)depuis un processus Java non-root est souvent sans effet. - Windows. Les priorités correspondent aux sept priorités de threads Windows. La correspondance est plus agressive ; les threads à haute priorité obtiennent effectivement plus de CPU. Mais on ne peut toujours pas préempter un thread à l'intérieur d'un appel système ou tenant un verrou noyau.
- macOS. Utilise
pthread_setschedparamcomme Linux ; comportement similaire.
La conclusion est la même sur tous : les priorités sont des conseils, et sur Linux, ces conseils sont souvent complètement ignorés. Ne concevez pas en comptant dessus.
Quand utiliser réellement setPriority
Trois usages légitimes, dans l'ordre de leur fréquence :
- Marquer un thread d'arrière-plan
MIN_PRIORITYpour que le système d'exploitation le déprioritise doucement sous charge. Un téléchargeur de télémétrie, un videur de journaux, un job de reconstruction de cache — aucun de ces threads ne devrait jamais causer de latence visible pour l'utilisateur. Les définir à 1 est une indication gratuite qui dit au système d'exploitation « celui-ci supporte d'être un peu mis à l'écart ». - Marquer un assistant temps-réel strict
MAX_PRIORITYdans une JVM qui a reçu le privilège de l'utiliser. Rare en dehors de l'audio, des boucles de jeu, ou de systèmes spécialisés à faible latence. - Ne pas le faire. La valeur par défaut
NORM_PRIORITYest la bonne réponse pour presque tous les threads que vous créerez jamais.
Ce que vous devriez utiliser à la place
Si la raison pour laquelle vous pensez aux priorités est l'une de celles-ci — il existe un meilleur outil :
| Vous voulez | Utilisez ceci à la place |
|---|---|
| « Les longs jobs ne devraient pas bloquer les courts » | Deux ExecutorServices — un petit pour les travaux sensibles à la latence, un grand pour les traitements par lots |
| « Le thread A doit s'exécuter avant le thread B » | join, un CountDownLatch, ou une chaîne CompletableFuture |
| « Un seul thread à la fois dans cette méthode » | synchronized ou un ReentrantLock |
| « Éviter d'affamer complètement les travaux à faible priorité » | Une file équitable (new LinkedBlockingQueue<>() plus une loterie, ou un PriorityBlockingQueue avec une priorité de tâche explicite — pas de priorité de thread) |
Le schéma : la priorité est une indication d'ordonnancement entre threads exécutables. Le bon outil pour « quel thread s'exécute en premier » est presque toujours la synchronisation, pas la priorité.
Inversion de priorité
Le mode de défaillance classique de l'ordonnancement basé sur les priorités :
- Un thread de faible priorité
Lacquiert le verrouM. - Un thread de haute priorité
Hessaie d'acquérirMet se bloque en attendantL. - Un thread de priorité moyenne
Medexécute un travail lié au CPU, empêchantLd'être ordonnancé. Hest effectivement à la priorité deMed— inversé.
Java ne résout pas cela pour vous. Le système nice du noyau non plus. La solution est soit (a) ne pas compter sur les priorités pour l'exactitude, soit (b) utiliser ReentrantLock avec une politique d'ordre équitable et des sections critiques courtes pour que la fenêtre d'inversion soit bornée. Nous verrons l'équité dans le chapitre ReentrantLock.
Groupes de threads (surtout historique)
ThreadGroup gp = new ThreadGroup("workers");
gp.setMaxPriority(7);
Thread t = new Thread(gp, runnable, "worker");
t.setPriority(10); // capped to 7 by the groupLes groupes de threads datent de Java 1.0 et étaient le « panneau de contrôle » d'origine pour les lots de threads. Ils ont été presque entièrement remplacés par ExecutorService et la plupart de leurs méthodes sont dépréciées. Vous verrez ThreadGroup dans les traces de pile et les dumps ; vous n'en construirez généralement pas. La seule partie encore active est le plafond maxPriority par groupe.
Un exemple pratique : essayer de voir les priorités à l'œuvre
Le programme ci-dessous génère plusieurs threads liés au CPU à différentes priorités et mesure la quantité de travail accomplie par chacun dans une fenêtre fixe. Sur Linux, vous verrez probablement qu'ils font tous des quantités de travail similaires — c'est le but. Sur Windows, vous pourrez voir une différence significative. Dans tous les cas, la leçon est la même.
Ce qu'il faut retenir de l'exécution :
- Les trois threads ont presque certainement accompli des quantités de travail similaires, même si l'un était à la priorité 1 et un autre à la priorité 10. Sur une JVM Linux exécutée en tant qu'utilisateur normal, la JVM ne peut pas réellement augmenter la priorité au-dessus de la valeur par défaut — l'appel
setPriority(10)a défini le champ mais le système d'exploitation l'a traité comme la priorité normale. Le champ au niveau Java change ; l'ordonnancement réel change à peine. - L'appel
getPriority()reflétait la valeur que vous aviez définie, peu importe si le système d'exploitation l'avait honorée. C'est important lors du débogage : le champ vous indique ce que votre code a demandé, pas ce que le système d'exploitation a fait. - La boucle CPU ne se bloquait intentionnellement jamais (pas de
Thread.sleep, pas dewait, pas d'E/S). C'est le seul scénario où la priorité peut vraisemblablement avoir de l'importance — la compétition CPU pure. Dès qu'un thread se bloque, la priorité devient sans importance ; le thread bloqué n'est de toute façon pas sur un CPU. setPriority(11)a levéIllegalArgumentException. Les bornes sont appliquées côté Java. La priorité 0 lève également une exception ; la plage valide est exactement de 1 à 10.- La bonne conclusion à tirer de la faible (ou inexistante) différence dans les comptes de lots est : lorsque votre conception commence à donner l'impression qu'elle a besoin de priorités pour l'exactitude, remplacez-la par une synchronisation explicite. Utilisez deux
ExecutorServices si vous voulez de l'isolation ; utilisez des verrous si vous voulez de l'ordre ; utilisez des files si vous voulez de l'équité. La priorité est un dernier recours, et sur Linux c'est à peine un recours.
Prochaine étape
Le chapitre suivant, Java Synchronization, commence la vraie histoire du code concurrent sûr — le mot-clé synchronized, les moniteurs intrinsèques, et le fonctionnement du primitive de verrou de première classe de Java.