Avatar ·

В Java: implements Runnable против extends Thread — что выбрать для создания потока?

Всем привет! Я начал изучать многопоточность в Java и запутался в двух подходах создания потоков. Везде пишут, что можно либо расширить класс Thread, либо реализовать интерфейс Runnable. Но когда что использовать? Вот примеры, которые я пробовал: 1) extends Thread: ```java class MyThread extends Thread { public void run() { System.out.println("Поток работает"); } } public class Main { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); } } ``` 2) implements Runnable: ```java class MyRunnable implements Runnable { public void run() { System.out.println("Поток работает"); } } public class Main { public static void main(String[] args) { MyRunnable task = new MyRunnable(); Thread t = new Thread(task); t.start(); } } ``` Оба варианта работают — я проверил. Но в чём разница? Почему в учебниках советуют использовать Runnable? И можно ли вообще наследовать Thread, если это «плохая практика»? Мне кажется, что extends Thread проще — меньше кода. Объясните, пожалуйста, новичку, какой подход лучше и почему. Заранее спасибо! <<>>

Почему implements Runnable — это правильный выбор в 95% случаев

Вы правы: оба варианта работают. Но разница не в функциональности, а в архитектуре и гибкости кода. Давайте разберёмся по порядку.

1. Наследование — это жёсткая связь

В Java класс может наследовать только один родительский класс. Когда вы пишете extends Thread, ваш класс становится потоком — и теряет возможность наследовать что-либо ещё. Например:

// Плохо: не можем наследовать другой полезный класс
class MyWorker extends Thread {
    // ... код
}

// Хорошо: можем наследовать что угодно
class MyWorker extends BaseWorker implements Runnable {
    // ... код
}

Если вашему классу нужно работать с GUI, базами данных или наследовать логику от другого класса — extends Thread станет непреодолимым барьером.

2. Runnable отделяет задачу от исполнителя

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

  • в отдельном потоке (new Thread(task).start())
  • в пуле потоков (ExecutorService.submit(task))
  • даже синхронно, просто вызвав task.run()

Пример гибкости:

Runnable task = () -> System.out.println("Привет из " + Thread.currentThread().getName());

// Вариант 1: в новом потоке
new Thread(task).start();

// Вариант 2: в пуле
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(task);
pool.shutdown();

// Вариант 3: прямо сейчас (для отладки)
task.run();

С extends Thread вы привязаны к созданию нового потока каждый раз.

3. Поток — это ресурс, а не задача

Создание потока — дорогая операция (выделение стека, системные вызовы). Thread — это тяжёлый объект, представляющий системный поток ОС. А Runnable — лёгкий интерфейс, описывающий логику. Смешивать эти понятия — плохая архитектура.

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

4. Когда всё-таки можно использовать extends Thread?

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

  • Изменение приоритета потока в конструкторе
  • Добавление логики в методы start() или interrupt()
  • Создание собственного пула потоков (но это уже для профи)

Пример (но всё равно лучше избегать):

class DaemonThread extends Thread {
    public DaemonThread() {
        setDaemon(true); // делаем поток демоном
    }
    public void run() {
        while (true) {
            // фоновая работа
        }
    }
}

5. Современные альтернативы

Начиная с Java 8, часто используют лямбды:

// Вместо создания отдельного класса Runnable
Thread t = new Thread(() -> System.out.println("Работаем"));
t.start();

// Или через ExecutorService
ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(() -> {
    // задача
});

Итог

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

Надеюсь, теперь разница стала понятна. Удачи в изучении многопоточности!

<<>>

Дополнение: производительность и пул потоков

Коллега выше отлично объяснил архитектурные причины. Я добавлю практический аспект — производительность.

Когда вы используете extends Thread и создаёте поток через new MyThread().start(), каждый раз происходит:

  1. Выделение памяти под стек потока (~512 КБ по умолчанию)
  2. Системный вызов для создания потока ОС
  3. Запуск планировщика

Если у вас 1000 коротких задач, вы создадите 1000 потоков — это убьёт производительность и может привести к OutOfMemoryError.

С Runnable вы можете передать задачу в пул потоков:

ExecutorService pool = Executors.newFixedThreadPool(10); // всего 10 потоков
for (int i = 0; i < 1000; i++) {
    final int taskId = i;
    pool.submit(() -> System.out.println("Задача " + taskId));
}
pool.shutdown();

Пул переиспользует 10 потоков для 1000 задач — это эффективно по памяти и времени. С extends Thread такой трюк невозможен.

Ещё один нюанс: Thread — это класс из JDK, и его наследование может сломаться при обновлении Java (например, если Oracle решит сделать конструктор приватным). Runnable — интерфейс, он стабилен.

Вывод: даже если ваш текущий проект мал, привыкайте к Runnable — это профессиональная привычка, которая окупится на больших проектах.