W3docs

Класс Thread в Java

Создание и управление потоками в Java через наследование Thread или передачу Runnable — и компромиссы между этими подходами.

java.lang.Thread — это объект, который вы держите в руках, когда хотите запустить, именовать, дождаться завершения, прервать или опросить поток выполнения. Предыдущая глава ввела потоки на концептуальном уровне; эта — обзор API. Всё в java.util.concurrent — исполнители, futures, виртуальные потоки — построено поверх Thread, поэтому стоит знать исходный класс, даже если в рабочем коде вы обычно будете тянуться за высокоуровневыми обёртками.

Два способа создать поток

Thread — это Runnable, обёрнутый в управляющий объект. Есть два способа передать ему Runnable:

// 1. Pass a Runnable to the constructor (the modern, preferred form)
Thread a = new Thread(() -> System.out.println("hello from " + Thread.currentThread().getName()));

// 2. Extend Thread and override run()
class HelloThread extends Thread {
  @Override public void run() {
    System.out.println("hello from " + getName());
  }
}
Thread b = new HelloThread();

Оба работают; оба запускают ваш код в новом потоке. Первая форма — то, что использует практически весь современный код, по трём причинам:

  • Класс может наследоваться только от одного другого класса. Если вы наследуете Thread, вы не можете наследовать больше ничего — а та часть вашего кода, которая является работой, почти никогда не имеет веской причины быть потоком в смысле ООП. Передача Runnable оставляет ваш бизнес-класс свободным.
  • Лямбды делают из формы с Runnable однострочник. Наследование Thread требует именованного класса для того же кода.
  • Runnable, который вы передаёте, можно также позже передать в ExecutorService. Подкласс Thread привязан к запуску в собственном выделенном потоке.

Наследуйте Thread только тогда, когда вам действительно нужно добавить состояние или методы к самому потоку (редкость). Во всех остальных случаях — передавайте Runnable.

Запуск и ожидание

Два метода, которые вы будете использовать практически с каждым потоком:

Thread t = new Thread(() -> doWork(), "worker");
t.start();                                    // schedule it; return immediately
t.join();                                     // block the caller until the thread finishes

Несколько ошибок, которые допускают новички:

  • start() — это то, что создаёт поток ОС. Прямой вызов run() выполняет тело в текущем потоке, синхронно — новый поток не создаётся. Это самая распространённая ошибка в многопоточном программировании для новичков. Если вы не видите start(), параллелизма не произошло.
  • start() можно вызвать только один раз. Thread — объект одноразового использования. Повторный вызов start() бросает IllegalThreadStateException. Чтобы снова запустить ту же задачу, создайте новый Thread или используйте ExecutorService.
  • join() может бросить InterruptedException. Это блокирующий вызов. Если кто-то вызывает interrupt() на потоке, ожидающем в join(), ожидание завершается с исключением. Вы обязаны его обработать или передать выше.

join(millis) ждёт не более указанного числа миллисекунд перед возвратом — независимо от того, завершился ли поток. Используйте его, когда хотите дать рабочему потоку ограниченный шанс завершиться корректно, прежде чем принять более жёсткие меры.

Конструкторы, которые важны

У Thread много конструкторов; на практике важны четыре:

КонструкторКогда использовать
new Thread(Runnable)Базовый случай. Анонимный рабочий поток.
new Thread(Runnable, String name)Почти всегда предпочтительнее — имена видны в логах, профайлерах, дампах потоков.
new Thread(ThreadGroup, Runnable, String)Когда нужна явная группа (редкость; группы в основном устарели).
new Thread(ThreadGroup, Runnable, String, long stackSize)Когда стек по умолчанию (~1 МБ) не подходит — например, при глубокой рекурсии или давлении на память.

Пустой конструктор new Thread() существует и запускает пустой run(), который ничего не делает. Использовать его нет смысла.

Всегда давайте потокам имена. "worker-1", "http-3", "flush-loop" — что угодно, отражающее роль. Дамп потоков, полный Thread-7, Thread-12, Thread-19 — это дамп, который невозможно прочитать.

Свойства экземпляра Thread

Несколько полей и геттеров, с которыми вы реально будете работать:

t.setName("scanner-2");                       // any time before or after start()
String name = t.getName();

t.setDaemon(true);                            // BEFORE start(); else IllegalThreadStateException
boolean d = t.isDaemon();

t.setPriority(Thread.NORM_PRIORITY);          // 1..10; mostly advisory, see chapter 6
int p = t.getPriority();

Thread.State s = t.getState();                // NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
boolean alive = t.isAlive();                  // true between start() and run() returning
long id = t.threadId();                       // Java 19+; old name: getId()

Два из них важнее всего:

  • setDaemon(true) определяет, удерживает ли поток JVM в живых. Смотрите предыдущую главу — демоны умирают вместе с программой; не-демоны держат её работающей, пока не вернутся.
  • getState() — то, на что вы смотрите в дампе потоков, чтобы диагностировать «почему поток завис». BLOCKED означает ожидание внутреннего замка; WAITING/TIMED_WAITING означает, что поток припаркован в wait(), join(), sleep(), LockSupport.park() и т. д.

Статические вспомогательные методы Thread

Несколько статических методов, которые вы будете вызывать изнутри рабочего потока:

Thread.currentThread();                       // the thread that's executing this code
Thread.sleep(2000);                           // pause this thread for ~2000 ms
Thread.yield();                               // hint to the scheduler "go ahead and run someone else"
Thread.interrupted();                         // returns and CLEARS the interrupt flag of currentThread

Thread.sleep — самый распространённый; он бросает InterruptedException, поэтому вызывающий код должен его обработать или передать выше. Thread.yield почти никогда не является правильным инструментом — это расплывчатая подсказка, которую JVM и ОС могут проигнорировать. Если нужна координация, используйте настоящий примитив синхронизации.

Thread.interrupted() возвращает true, если текущий поток был прерван, и сбрасывает флаг. t.isInterrupted() (метод экземпляра, на другом потоке) возвращает флаг без сброса. Путаница между ними — распространённый источник застрявших прерываний.

Прерывание: как попросить поток остановиться

Безопасного t.stop() не существует (метод есть, но он устарел с версии 1.1, потому что оставляет замки захваченными, а состояние — испорченным). Кооперативный протокол завершения таков:

Thread worker = new Thread(() -> {
  while (!Thread.currentThread().isInterrupted()) {
    doOneUnitOfWork();
  }
}, "worker");
worker.start();
// ... later, from somewhere else:
worker.interrupt();
worker.join();

interrupt() устанавливает флаг прерывания рабочего потока. Предполагается, что рабочий поток проверяет флаг в безопасных точках и выходит. Если рабочий поток заблокирован в sleep, wait, join или многих вызовах java.nio, блокирующий вызов немедленно бросает InterruptedException, чтобы поток мог отреагировать.

Если вы перехватываете InterruptedException и не хотите передавать его выше, принято повторно устанавливать флаг, чтобы вызывающий код выше по стеку всё ещё видел прерывание:

try { Thread.sleep(1000); }
catch (InterruptedException e) {
  Thread.currentThread().interrupt();         // re-arm the flag
  return;                                     // and give up cooperatively
}

Проглотить прерывание без повторной установки флага — это баг. Флаг — это то, как остальная программа знает, что поток попросили остановиться.

Развёрнутый пример: полный жизненный цикл в одной программе

Программа ниже создаёт двух рабочих разными способами (Runnable, подкласс), наблюдает их переходы состояний, дожидается их завершения и демонстрирует протокол прерывания на третьем рабочем потоке.

java— editable, runs on the server

Что следует вынести из запуска:

  • Переходы состояний соответствуют контракту. До start() оба потока были в состоянии NEW. После start()RUNNABLE (или TERMINATED, если работа была крошечной и завершилась до вывода). После join() оба перешли в TERMINATED. Именно это описывает Thread.State.
  • Строка "t3 ran on thread: main" — это баг, который нужно запомнить навсегда. t3.run() выполнил тело — в вызывающем потоке, синхронно. Новый поток создан не был. После этого t3.isAlive() вернул false, потому что start() никогда не вызывался. Если вы отлаживаете «ничто, кажется, не выполняется параллельно», проверьте, написали ли вы start() или run().
  • Цикл с прерыванием не использовал Thread.sleep как основное ожидание — он просто опрашивал флаг в цикле, с редкими короткими усыплениями, чтобы прерывание могло завершить сон досрочно. Контракт одинаков в обоих случаях: isInterrupted() — это то, что опрашивает рабочий поток; interrupt() — то, что вызывает запрашивающий.
  • Повторная установка флага внутри catch (строка Thread.currentThread().interrupt()) сохранила сигнал для любого кода выше по стеку вызовов. Без этой строки перехваченное и проигнорированное прерывание бесследно исчезнет — это один из самых простых способов написать поток, который не завершится корректно.
  • Демон в конце был готов спать 60 секунд; вместо этого JVM завершилась, как только main вернулся, убив его на полуслове. Потоки-демоны могут владеть любыми ресурсами — но их также могут прервать в любой момент, поэтому не стоит поручать им работу, требующую фиксации результатов.

Что дальше

Следующая глава, Интерфейс Runnable в Java, подробно рассматривает Runnable — что это на самом деле, почему поверх него были добавлены Callable и Future, и как лямбды изменили эргономику передачи работы потоку.

Практика

Практика
Вы вызываете `t.run()` (не `t.start()`) на `Thread`, чей `Runnable` печатает имя текущего потока. Что он напечатает?
Вы вызываете `t.run()` (не `t.start()`) на `Thread`, чей `Runnable` печатает имя текущего потока. Что он напечатает?
Was this page helpful?