Priorità dei Thread in Java
Le priorità dei thread Java sono un suggerimento, non una garanzia — cosa fa setPriority, cosa non fa e quando usarlo.
Thread porta una priorità — un intero da 1 a 10. La JVM la passa allo scheduler del sistema operativo come suggerimento su quale thread eseguire quando la CPU deve scegliere. Il suggerimento è solo quello — un suggerimento. Il sistema operativo è libero di ignorarlo e sulla maggior parte dei sistemi operativi desktop e server l'effetto è compreso tra impercettibile e invisibile. Questo capitolo spiega come appare l'API, cosa accade effettivamente sotto il cofano e i casi molto rari in cui impostare una priorità vale la pena.
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;Tre costanti per la leggibilità — la maggior parte del codice che tocca la priorità usa queste anziché interi grezzi:
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();Il costruttore usa la priorità del thread padre e la priorità massima del gruppo di thread del padre — di solito 5. Impostare un valore fuori dall'intervallo 1..10 lancia un'eccezione IllegalArgumentException.
Cosa significa "priorità" per la JVM
Due cose, nessuna delle quali è "questo thread verrà eseguito per primo":
- Un suggerimento di scheduling nativo. La JVM traduce il numero da 1 a 10 in qualcosa che il sistema operativo host comprende. Su Linux è un valore
nicetramitepthread_setschedparam; su Windows è una costanteTHREAD_PRIORITY_*. La mappatura è definita dall'implementazione della JVM. - Un limite massimo per gruppo di thread. Un thread non può essere impostato a una priorità superiore al
maxPrioritydel suo gruppo di thread. I gruppi di thread sono per lo più deprecati, ma il limite si applica ancora.
Cosa la priorità non significa:
- Non è un "blocco" — un thread ad alta priorità non può pre-emere un thread dentro una sezione critica.
- Non è una garanzia di ordinamento. Due thread alla priorità 10 si contendono comunque la CPU; uno di essi la ottiene per primo.
- Non influisce sulla correttezza. Qualsiasi programma che "funziona solo" grazie alle priorità ha un vero bug di sincronizzazione nascosto dietro.
Cosa fa effettivamente il sistema operativo
I diversi sistemi operativi host trattano il suggerimento in modo diverso. Il comportamento attuale, in linea di massima:
- Linux. La configurazione predefinita della JVM tratta le priorità Java come valori
nice.niceè un suggerimento allo scheduler CFS riguardo alla quota di CPU. Gli utenti non-root possono solo abbassare la priorità (nicepositivo); alzarla (nicenegativo) richiedeCAP_SYS_NICE, che la maggior parte delle JVM non ha. Quindi in pratica,setPriority(MAX_PRIORITY)da un processo Java non-root è spesso un no-op. - Windows. Le priorità vengono mappate alle sette priorità di thread di Windows. La mappatura è più aggressiva; i thread ad alta priorità ottengono davvero più CPU. Ma non si può comunque pre-emere un thread dentro una syscall o che tiene un lock del kernel.
- macOS. Usa
pthread_setschedparamcome Linux; comportamento simile.
La conclusione è la stessa su tutti: le priorità sono consigli, e su Linux il consiglio viene spesso ignorato del tutto. Non progettare per questo.
Quando usare effettivamente setPriority
Tre usi legittimi, in ordine di frequenza con cui si presentano:
- Contrassegnare un thread in background con
MIN_PRIORITYcosì il sistema operativo lo deprioritizza dolcemente sotto carico. Un uploader di telemetria, un flusher di log, un job di rebuild della cache — nessuno di questi dovrebbe mai causare latenza visibile all'utente. Impostarli a 1 è un suggerimento gratuito che dice al sistema operativo "questo va bene privarlo un po' di risorse." - Contrassegnare un helper hard-realtime con
MAX_PRIORITYin una JVM a cui è stato dato il privilegio di usarlo. Raro al di fuori di audio, game loop o sistemi specializzati a bassa latenza. - Non farlo. Il valore predefinito di
NORM_PRIORITYè la risposta giusta per quasi ogni thread che creerai.
Cosa dovresti usare al suo posto
Se il motivo per cui stai pensando alle priorità è uno di questi — c'è uno strumento migliore:
| Vuoi | Usa questo invece |
|---|---|
| "I job lunghi non devono bloccare quelli brevi" | Due ExecutorService — uno piccolo per il lavoro sensibile alla latenza, uno grande per i batch |
| "Il thread A deve girare prima del thread B" | join, un CountDownLatch, o una catena di CompletableFuture |
| "Solo un thread alla volta in questo metodo" | synchronized o un ReentrantLock |
| "Evitare di affamare completamente il lavoro a bassa priorità" | Una coda fair (new LinkedBlockingQueue<>() più una lotteria, o una PriorityBlockingQueue con priorità esplicita del task — non del thread) |
Il pattern: la priorità è un suggerimento di scheduling tra thread eseguibili. Lo strumento giusto per "quale thread gira per primo" è quasi sempre la sincronizzazione, non la priorità.
Inversione di priorità
Il classico fallimento dello scheduling basato su priorità:
- Un thread a bassa priorità
Lacquisisce il lockM. - Un thread ad alta priorità
Htenta di acquisireMe rimane bloccato ad aspettareL. - Un thread a priorità media
Medesegue lavoro CPU-bound, impedendo aLdi essere schedulato. Hè di fatto alla priorità diMed— invertita.
Java non risolve questo per te. Il sistema nice del kernel nemmeno. La soluzione è (a) non fare affidamento sulle priorità per la correttezza, oppure (b) usare ReentrantLock con una politica di ordinamento fair e sezioni critiche brevi così la finestra di inversione è delimitata. Vedremo la fairness nel capitolo ReentrantLock.
Gruppi di thread (principalmente storici)
ThreadGroup gp = new ThreadGroup("workers");
gp.setMaxPriority(7);
Thread t = new Thread(gp, runnable, "worker");
t.setPriority(10); // capped to 7 by the groupI gruppi di thread risalgono a Java 1.0 ed erano il "pannello di controllo" originale per lotti di thread. Sono stati quasi interamente sostituiti da ExecutorService e la maggior parte dei loro metodi è deprecata. Vedrai ThreadGroup in stack trace e dump; di solito non ne costruirai uno. L'unica parte ancora attiva è il limite maxPriority per gruppo.
Un esempio pratico: cercare di vedere le priorità in azione
Il programma seguente avvia diversi thread CPU-bound a diverse priorità e misura quanto lavoro ciascuno ha svolto in una finestra fissa. Su Linux probabilmente vedrai tutti fare quantità simili di lavoro — questo è il punto. Su Windows potresti vedere una differenza significativa. In ogni caso, la lezione è la stessa.
Cosa ricavare dall'esecuzione:
- Tutti e tre i thread quasi certamente hanno svolto quantità simili di lavoro, anche se uno era alla priorità 1 e un altro alla priorità 10. Su una JVM Linux in esecuzione come utente normale, la JVM non può effettivamente alzare la priorità sopra il valore predefinito — la chiamata
setPriority(10)ha impostato il campo ma il sistema operativo lo ha trattato come la priorità normale. Il campo a livello Java cambia; lo scheduling effettivo quasi per niente. - La chiamata
getPriority()ha riflesso il valore impostato, indipendentemente dal fatto che il sistema operativo lo abbia rispettato. Questo è importante durante il debug: il campo ti dice cosa il tuo codice ha chiesto, non cosa il sistema operativo ha fatto. - Il ciclo CPU intenzionalmente non si è mai bloccato (nessun
Thread.sleep, nessunwait, nessun I/O). Questo è l'unico scenario in cui la priorità può plausibilmente avere importanza — pura competizione per la CPU. Non appena un thread si blocca, la priorità diventa irrilevante; il thread bloccato non è su una CPU comunque. setPriority(11)ha lanciatoIllegalArgumentException. I limiti sono applicati lato Java. Anche la priorità 0 lancia un'eccezione; l'intervallo valido è esattamente da 1 a 10.- La giusta conclusione dalla differenza piccola (o inesistente) nel conteggio dei batch è: quando il tuo design comincia a sembrare che abbia bisogno delle priorità per la correttezza, sostituiscilo con la sincronizzazione esplicita. Usa due
ExecutorServicese vuoi isolamento; usa i lock se vuoi ordinamento; usa le code se vuoi fairness. La priorità è un ultimo ricorso e su Linux è a malapena un ricorso.
Cosa fare dopo
Il prossimo capitolo, Java Synchronization, inizia la vera storia del codice concorrente sicuro — la parola chiave synchronized, i monitor intrinseci e come funziona il primitivo di lock di prima classe di Java.