W3docs

Приоритет потоков в 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

Две вещи, ни одна из которых не является «этот поток запустится первым»:

  1. Подсказка нативному планировщику. JVM транслирует число 1–10 в нечто понятное хост-ОС. В Linux это значение nice через pthread_setschedparam; в Windows — константа THREAD_PRIORITY_*. Отображение определяется реализацией JVM.
  2. Максимальный порог для группы потоков. Поток не может быть установлен в приоритет выше 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

Три законных варианта применения, в порядке частоты встречаемости:

  1. Пометить фоновый поток как MIN_PRIORITY, чтобы ОС мягко депривилегировала его под нагрузкой. Загрузчик телеметрии, сбросчик журналов, задача пересборки кэша — ни один из них не должен влиять на пользовательскую задержку. Установка значения 1 — бесплатная подсказка ОС «с этим можно немного повременить».
  2. Пометить реального кандидата на жёсткий реальный режим как MAX_PRIORITY в JVM с соответствующими привилегиями. Редко встречается вне аудио, игровых циклов или специализированных систем с малой задержкой.
  3. Не делать этого. Значение по умолчанию 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 разница может быть ощутимой. В любом случае урок одинаков.

java— editable, runs on the server

Что можно вынести из запуска:

  • Все три потока почти наверняка выполнили примерно одинаковый объём работы, хотя один имел приоритет 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.

Практика

Практика
Вы работаете в Linux и запускаете Java-программу от обычного пользователя. Вы вызываете `t.setPriority(Thread.MAX_PRIORITY)` на рабочем потоке. Что реально происходит с планированием этого потока на уровне ОС?
Вы работаете в Linux и запускаете Java-программу от обычного пользователя. Вы вызываете `t.setPriority(Thread.MAX_PRIORITY)` на рабочем потоке. Что реально происходит с планированием этого потока на уровне ОС?
Was this page helpful?