Thread и Runnable: создание потока

В исходной форме заметки было два способа: унаследовать Thread или передать Runnable. Оба работают, но решают разные задачи:

  • Thread — механизм исполнения и его жизненный цикл;
  • Runnable — задача без результата.

Отдельный Thread

Thread worker = new Thread(
        () -> System.out.println(Thread.currentThread().getName()),
        "report-worker"
);
worker.start();
worker.join();

start() просит JVM создать новый путь исполнения, который затем вызовет run(). Для обычного platform thread прямой вызов worker.run() — вызов метода в текущем потоке, а не запуск. У virtual thread (Java 21+) прямой вызов run() вообще не исполняет переданную задачу: пользоваться нужно start().

Один объект Thread запускают только один раз. Повторный start() завершится IllegalThreadStateException.

Наследование оправдано редко:

final class LoggingThread extends Thread {
    @Override
    public void run() {
        // задача
    }
}

Оно связывает задачу с инфраструктурой, занимает единственное наследование класса и усложняет повторное использование.

Задача отдельно от исполнителя

Runnable cleanup = () -> clearExpiredEntries();
 
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
    executor.execute(cleanup);
} finally {
    executor.shutdown();
}

Для серверного кода это обычный выбор: пул ограничивает число потоков, переиспользует их и принимает политику очереди. Если нужен результат или checked exception, используется Callable.

Что часто путают

  • start() не гарантирует, что новый поток начнет работу раньше следующей строки текущего потока.
  • Объект Runnable сам по себе не поток: один экземпляр можно выполнить разными исполнителями.
  • Thread нельзя безопасно остановить извне; нужен протокол прерывания — Остановка потока.
  • «Полный контроль через наследование Thread» — не преимущество: все методы доступны у объекта Thread и при композиции.

На собеседовании

  • Чем start() отличается от run()? start() создает новый путь исполнения; run() выполняется как обычный метод.
  • Можно ли запустить один Thread дважды? Нет.
  • Почему обычно выбирают Runnable? Он разделяет задачу и способ исполнения.
  • Когда нужен Callable? Когда задача возвращает результат или бросает checked exception.
  • Нужно ли создавать поток на каждую короткую задачу? Обычно нет, используют ограниченный executor.