Методы Java Thread
Основные методы управления потоками Java — start, run, sleep, join, interrupt, yield, setDaemon — с разбором типичных ошибок в реальном коде.
У Thread много методов, но лишь немногие из них вы будете вызывать в реальном коде. В этой главе рассматривается именно этот набор — что делает каждый метод, что он не делает, и какие ошибки выглядят корректными до тех пор, пока они не проявляются. В предыдущих главах эти методы упоминались вскользь; здесь каждый из них разобран подробно.
Эта страница охватывает жизненный цикл потока и управление им: start vs run, sleep, join, кооперативный протокол отмены interrupt, а также вспомогательные методы (yield, currentThread, именование, статус демона, приоритет, holdsLock, onSpinWait). Для общего представления о том, как поток переходит между состояниями NEW, RUNNABLE, BLOCKED, WAITING и TERMINATED, см. Жизненный цикл потока Java.
start() vs. run()
Самая распространённая ошибка в многопоточности:
Thread t = new Thread(() -> work(), "worker");
t.run(); // wrong: runs work() on the CURRENT thread
t.start(); // right: spawns a new OS thread, returns immediatelystart() — единственный метод, который создаёт новый поток ОС. run() — это тело выполняемой работы; вызов его напрямую — это обычный вызов метода, который возвращает управление, когда работа завершена. Если вы не видите start(), никакого параллелизма нет.
start() также можно вызвать лишь один раз. После возврата из run() поток находится в состоянии TERMINATED и не может быть перезапущен. Повторный вызов start() выбрасывает IllegalThreadStateException.
Thread.sleep(ms)
Статический вызов, который блокирует текущий поток не менее чем на указанное время:
Thread.sleep(1500); // sleep 1.5 seconds
Thread.sleep(0, 250); // 250 nanoseconds; precision varies by OS
Thread.sleep(Duration.ofMillis(1500)); // Java 19+ overloadТри важных момента:
- Метод выбрасывает
InterruptedException. Сон прерываем — именно так рабочий поток получает сигнал прекратить ожидание и завершить работу. Нужно либо пробросить исключение (объявитьthrows), либо поймать его и восстановить флаг с помощьюThread.currentThread().interrupt(). - Метод не освобождает блокировки. Спящий поток удерживает все блокировки, которые держал до этого. Если вы вызываете
Thread.sleepвнутри блокаsynchronized, ни один другой поток не войдёт в этот блок, пока вы спите. Это почти всегда ошибка; используйтеwaitилиCondition.await(см. Межпоточное взаимодействие), когда нужно освободить блокировку. - Время — «не менее», а не «ровно». ОС может разбудить вас немного позже под нагрузкой; но никогда раньше.
t.join() и t.join(ms)
Ожидание завершения другого потока:
t.join(); // block until t terminates
t.join(2000); // block up to 2 seconds, then continue regardless
boolean done = t.join(Duration.ofSeconds(2));// Java 19+, returns whether it finishedjoin — это способ организовать многоэтапную параллельную работу: запустить несколько потоков, дать им выполниться, вызвать join для каждого и прочитать результаты. join() возвращает управление, когда метод run() целевого потока завершился (как нормально, так и с исключением). Он также выбрасывает InterruptedException, так что вызывающий код может быть прерван во время ожидания.
Тонкость: join(0) означает «join без таймаута» (то есть ждать вечно), а не «join с нулевым таймаутом». Если нужно проверить «завершился ли поток прямо сейчас», используйте t.isAlive().
t.interrupt() и флаг прерывания
Кооперативный протокол отмены, состоящий из трёх вызовов:
t.interrupt(); // set t's interrupt flag (and unblock sleep/wait/join/park)
t.isInterrupted(); // ask whether the flag is set (does NOT clear)
Thread.interrupted(); // static; ask current thread, and CLEAR the flagФлаг — это просто volatile boolean в объекте Thread. interrupt() устанавливает его. Если в данный момент поток t находится в sleep, wait, join или LockSupport.park (или во многих блокирующих вызовах java.nio), этот блокирующий вызов немедленно выбросит InterruptedException. В противном случае флаг ждёт, пока рабочий поток сам его не заметит.
Рабочий поток, который хочет поддерживать прерывание, несёт две обязанности:
- Проверять
Thread.currentThread().isInterrupted()между длительными шагами. - В каждом блоке
catch (InterruptedException e)либо пробрасывать исключение, либо восстанавливать флаг с помощьюThread.currentThread().interrupt()— никогда не проглатывать молча.
while (!Thread.currentThread().isInterrupted()) {
try {
doOneUnit();
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore flag for the loop check
}
}Thread.yield() — почти никогда не нужный инструмент
Thread.yield(); // hint: please run someone elseНеобязательная подсказка планировщику. ОС вправе её проигнорировать. В реальном производственном коде практически нет случаев, когда нужен yield — если вы ждёте события, используйте wait, Condition или Semaphore. К yield следует прибегать лишь в микробенчмарках, тестовых стендах для обнаружения дедлоков или при написании самой JVM.
Thread.currentThread()
Статический метод доступа к потоку, в котором выполняется вызывающий код. Два основных применения:
String who = Thread.currentThread().getName();
Thread.currentThread().interrupt(); // re-arm the flag after catching InterruptedExceptiongetName() — стандартный способ помечать строки в логах, чтобы отличить потоки друг от друга в выводе в продакшене.
getName / setName
Имена важны для отладки. Имя по умолчанию (Thread-3) бесполезно в дампе потоков.
Thread t = new Thread(this::flush, "flush-loop"); // name at construction (preferred)
t.setName("flush-loop-2"); // rename later if a role changesПереименовать можно в любой момент, но читатель увидит значение, актуальное в момент дампа или записи в лог. Всегда передавайте имя в конструктор.
setDaemon(true)
Thread t = new Thread(this::poll, "metrics-poller");
t.setDaemon(true); // BEFORE start(); else IllegalThreadStateException
t.start();Потоки-демоны не удерживают JVM от завершения — когда завершается последний не-демонный поток, JVM принудительно останавливает все демоны. Используйте их для фоновых задач обслуживания, которые должны завершаться вместе с программой (таймеры, отправка метрик, циклы опроса). Не используйте их для работы, завершение которой вам действительно важно.
setPriority(int)
t.setPriority(Thread.MAX_PRIORITY); // 10
t.setPriority(Thread.MIN_PRIORITY); // 1
t.setPriority(Thread.NORM_PRIORITY); // 5 (default)В основном носит рекомендательный характер. В следующей главе приоритеты рассматриваются подробно; главное, что нужно знать сейчас: не полагайтесь на них для обеспечения корректности — ОС сама решает, что они означают.
Thread.holdsLock(obj)
Статический вспомогательный метод для отладки:
assert Thread.holdsLock(monitor) : "expected to be inside a synchronized block on monitor";Возвращает true, если вызывающий поток держит внутренний монитор объекта obj. Полезно для утверждения «этот метод должен вызываться только внутри блока synchronized» без накладных расходов на захват блокировки при нормальном выполнении.
Thread.onSpinWait() — Java 9+
while (!done) {
Thread.onSpinWait(); // hint to the CPU: I'm spinning, slow down
}Подсказка на уровне процессора, которая приостанавливает конвейер и снижает энергопотребление в узком цикле ожидания. Это инструмент для очень узкого случая: когда вы крутите цикл несколько микросекунд в ожидании, пока другой поток изменит флаг; это не универсальный «освободи CPU» вызов. Для более длительного ожидания используйте LockSupport.park или Condition.
Пример с большинством методов в одном месте
Программа ниже использует start, join с таймаутом, interrupt, sleep, isInterrupted и setName вместе — методы, которые вы действительно будете вызывать в продакшене.
Что следует извлечь из запуска:
- Строка
bad.run()вывелаran on: main. Новый поток не был создан.bad.isAlive()былоfalseпосле, потому чтоstart()так и не был вызван. В каждой многопоточной программе эта ошибка встречается хотя бы раз; сделав её однажды, вы уже не повторите. slow.join(300)вернул управление примерно через 300 мс, хотяslowдолжен был спать 2000 мс.isAlive()оставалосьtrue.join(ms)— это ожидание с ограничением по времени; полезно, когда вы хотите дать рабочему потоку шанс завершиться корректно, прежде чем предпринять другие действия.slow.interrupt()немедленно завершилThread.sleepвнутри рабочего потока, выбросивInterruptedException. Таков контракт: прерываемые блокирующие вызовы реагируют наinterrupt(), прекращая блокировку с исключением — именно так работает кооперативная отмена на практике.- Рабочий поток
bookkeeperпоймалInterruptedExceptionи восстановил флаг с помощьюThread.currentThread().interrupt(). Последующий вызовisInterrupted()вернулtrue. Без этого восстановления флаг теряется, и любой код выше по стеку вызовов будет думать, что прерывания не было. daemon.setDaemon(true)был вызван доstart()— вызов после привёл бы кIllegalThreadStateException. И когдаmainзавершился, демон был остановлен прямо во время сна; JVM завершила работу, потому что не осталось ни одного не-демонного потока. Это компромисс демонов: никогда не блокирует завершение JVM, никогда не гарантирует своё завершение.
Что дальше
В следующей главе, Приоритет потоков Java, рассматривается метод setPriority класса Thread, что эти приоритеты на самом деле означают в реальных ОС и почему следует воспринимать их как подсказку, а не как гарантию.