Приоритет потоков в Java
Приоритеты потоков Java — это подсказка, а не гарантия: что делает setPriority и когда его стоит применять.
Thread хранит приоритет — целое число от 1 до 10. JVM передаёт его планировщику ОС в качестве подсказки о том, какой поток запустить, когда процессор должен сделать выбор. Это именно подсказка — не более. ОС вправе её проигнорировать, и на большинстве настольных и серверных ОС эффект где-то между незначительным и невидимым. В этой главе объясняется, как выглядит API, что происходит под капотом и в каких крайне редких случаях установка приоритета оправдана.
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;Три константы для удобочитаемости — большинство кода, работающего с приоритетами, использует их, а не числа напрямую:
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();Конструктор берёт приоритет родительского потока и максимальный приоритет группы родительского потока — как правило 5. Установка значения вне диапазона 1..10 бросает IllegalArgumentException.
Что «приоритет» означает для JVM
Две вещи, ни одна из которых не является «этот поток запустится первым»:
- Подсказка нативному планировщику. JVM транслирует число 1–10 в нечто понятное хост-ОС. В Linux это значение
niceчерезpthread_setschedparam; в Windows — константаTHREAD_PRIORITY_*. Отображение определяется реализацией JVM. - Максимальный порог для группы потоков. Поток не может быть установлен в приоритет выше
maxPriorityсвоей группы. Группы потоков в основном устарели, но ограничение по-прежнему работает.
Что приоритет не означает:
- Это не «блокировка» — высокоприоритетный поток не может вытеснить поток, находящийся в критической секции.
- Это не гарантия порядка. Два потока с приоритетом 10 всё равно соревнуются; один из них получит CPU первым.
- Это не влияет на корректность. Любая программа, которая «работает только» из-за приоритетов, скрывает в себе реальную ошибку синхронизации.
Что ОС на самом деле делает
Разные хост-ОС обрабатывают подсказку по-разному. Текущее поведение, в общих чертах:
- Linux. Конфигурация JVM по умолчанию трактует приоритеты Java как значения
nice.nice— подсказка планировщику CFS о доле CPU. Непривилегированные пользователи могут только понижать приоритет (положительныйnice); повышение (отрицательныйnice) требуетCAP_SYS_NICE, которого у большинства JVM нет. Поэтому на практикеsetPriority(MAX_PRIORITY)из непривилегированного Java-процесса нередко является холостым вызовом. - Windows. Приоритеты отображаются на семь уровней приоритетов потоков Windows. Отображение более агрессивное; высокоприоритетные потоки действительно получают больше CPU. Но вы всё равно не можете вытеснить поток внутри системного вызова или удерживающий блокировку ядра.
- macOS. Использует
pthread_setschedparam, как Linux; поведение аналогичное.
Вывод одинаков на всех платформах: приоритеты — это совет, и в Linux этот совет нередко полностью игнорируется. Не проектируйте систему с расчётом на него.
Когда реально использовать setPriority
Три законных варианта применения, в порядке частоты встречаемости:
- Пометить фоновый поток как
MIN_PRIORITY, чтобы ОС мягко депривилегировала его под нагрузкой. Загрузчик телеметрии, сбросчик журналов, задача пересборки кэша — ни один из них не должен влиять на пользовательскую задержку. Установка значения 1 — бесплатная подсказка ОС «с этим можно немного повременить». - Пометить реального кандидата на жёсткий реальный режим как
MAX_PRIORITYв JVM с соответствующими привилегиями. Редко встречается вне аудио, игровых циклов или специализированных систем с малой задержкой. - Не делать этого. Значение по умолчанию
NORM_PRIORITY— правильный ответ почти для каждого потока, который вы когда-либо создадите.
Что использовать вместо приоритетов
Если причина, по которой вы думаете о приоритетах, — одна из следующих, есть инструмент лучше:
| Вам нужно | Используйте вместо этого |
|---|---|
| «Длинные задания не должны блокировать короткие» | Два ExecutorService — небольшой для чувствительной к задержке работы, большой для пакетной |
| «Поток A должен запуститься раньше потока B» | join, CountDownLatch или цепочка CompletableFuture |
| «Только один поток за раз в этом методе» | synchronized или ReentrantLock |
| «Не дать низкоприоритетной работе полностью проголодать» | Честная очередь (new LinkedBlockingQueue<>() плюс лотерея, или PriorityBlockingQueue с явным приоритетом задачи — не потока) |
Паттерн: приоритет — это подсказка планировщику между запущенными потоками. Правильный инструмент для «какой поток запустится первым» — почти всегда синхронизация, а не приоритет.
Инверсия приоритета
Классический сценарий сбоя при планировании на основе приоритетов:
- Низкоприоритетный поток
Lзахватывает блокировкуM. - Высокоприоритетный поток
Hпытается захватитьMи блокируется в ожиданииL. - Среднеприоритетный поток
Medвыполняет CPU-интенсивную работу, не даваяLполучить расписание. Hфактически работает с приоритетомMed— инвертированным.
Java не решает эту проблему за вас. Система nice ядра тоже. Исправление: либо (a) не полагайтесь на приоритеты для корректности, либо (b) используйте ReentrantLock с политикой честной очерёдности и короткими критическими секциями, чтобы окно инверсии было ограничено. Честность мы рассмотрим в главе ReentrantLock.
Группы потоков (в основном исторически)
ThreadGroup gp = new ThreadGroup("workers");
gp.setMaxPriority(7);
Thread t = new Thread(gp, runnable, "worker");
t.setPriority(10); // capped to 7 by the groupГруппы потоков появились в Java 1.0 и были первоначальной «панелью управления» для партий потоков. Они почти полностью вытеснены ExecutorService, а большинство их методов устарело. Вы встретите ThreadGroup в трассировках стека и дампах; обычно вы их не создаёте. Единственная активная часть — ограничение maxPriority на уровне группы.
Практический пример: попытка увидеть приоритеты в работе
Программа ниже запускает несколько CPU-интенсивных потоков с разными приоритетами и измеряет, сколько работы каждый выполнил за фиксированное окно. На Linux вы, скорее всего, увидите схожие результаты — это и есть суть. На Windows разница может быть ощутимой. В любом случае урок одинаков.
Что можно вынести из запуска:
- Все три потока почти наверняка выполнили примерно одинаковый объём работы, хотя один имел приоритет 1, а другой — 10. На Linux JVM, запущенной от обычного пользователя, JVM фактически не может поднять приоритет выше нормального — вызов
setPriority(10)установил поле, но ОС восприняла это как обычный приоритет. Поле на уровне Java меняется; реальное планирование — почти нет. - Вызов
getPriority()отразил установленное вами значение, независимо от того, учла ли его ОС. Это важно при отладке: поле говорит вам, что запросил ваш код, а не что сделала ОС. - CPU-цикл намеренно никогда не блокировался (нет
Thread.sleep,wait, нет ввода-вывода). Это единственный сценарий, в котором приоритет теоретически может иметь значение — чистая конкуренция за CPU. Как только поток блокируется, приоритет становится неактуальным: заблокированный поток всё равно не занимает CPU. setPriority(11)бросилIllegalArgumentException. Границы проверяются на стороне Java. Приоритет 0 тоже бросает; допустимый диапазон — ровно от 1 до 10.- Правильный вывод из небольшой (или несуществующей) разницы в количестве пакетов: когда ваша архитектура начинает нуждаться в приоритетах для корректности, замените это явной синхронизацией. Используйте два
ExecutorServiceдля изоляции; используйте блокировки для упорядочивания; используйте очереди для честности. Приоритет — крайняя мера, а в Linux это почти и не мера вовсе.
Что дальше
Следующая глава, Java Synchronization, начинает настоящую историю о безопасном параллельном коде — ключевое слово synchronized, встроенные мониторы и то, как работает примитив блокировки первого класса в Java.