W3docs

Методы 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 immediately

start() — единственный метод, который создаёт новый поток ОС. 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 finished

join — это способ организовать многоэтапную параллельную работу: запустить несколько потоков, дать им выполниться, вызвать 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. В противном случае флаг ждёт, пока рабочий поток сам его не заметит.

Рабочий поток, который хочет поддерживать прерывание, несёт две обязанности:

  1. Проверять Thread.currentThread().isInterrupted() между длительными шагами.
  2. В каждом блоке 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 InterruptedException

getName() — стандартный способ помечать строки в логах, чтобы отличить потоки друг от друга в выводе в продакшене.

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 вместе — методы, которые вы действительно будете вызывать в продакшене.

java— editable, runs on the server

Что следует извлечь из запуска:

  • Строка 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, что эти приоритеты на самом деле означают в реальных ОС и почему следует воспринимать их как подсказку, а не как гарантию.

Практика

Практика
Вы перехватываете `InterruptedException` в рабочем потоке, но не хотите выбрасывать исключение из цикла. Что делать с флагом прерывания?
Вы перехватываете `InterruptedException` в рабочем потоке, но не хотите выбрасывать исключение из цикла. Что делать с флагом прерывания?
Was this page helpful?