Extends Thread или implements Runnable: как не провалить собеседование

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

implements Runnable vs extends Thread: почему 90% Java-разработчиков ошибаются?

Представьте: вы на собеседовании в крупную IT-компанию. Эйчар улыбается и задаёт "простой" вопрос: "Чем отличается implements Runnable от extends Thread в Java?". Казалось бы, что тут сложного? Но именно здесь, по данным Stack Overflow, спотыкаются даже опытные разработчики. Тысячи просмотров, сотни комментариев — тема "Java Runnable vs Thread" остаётся одной из самых горячих.

В этой статье мы не просто перечислим различия. Мы разберём топ-5 реальных ошибок, которые допускают программисты, и покажем, как писать чистый, масштабируемый многопоточный код. Если вы пишете на Java и используете Thread напрямую — читайте внимательно. Возможно, вы сами не замечаете, как создаёте себе проблемы.

Краткий ликбез: что такое Thread и Runnable?

Прежде чем нырять в дебри, давайте освежим базу. Java Thread — это класс, представляющий поток выполнения. Он умеет запускать код в параллельном режиме. Runnable — это функциональный интерфейс с единственным методом run(). Он описывает задачу, которую нужно выполнить, но не знает, как именно её запускать.

Вот классический пример с extends Thread:

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Поток работает: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); // Запуск нового потока
    }
}

А вот тот же функционал через implements Runnable:

public class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Задача выполняется: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t = new Thread(new MyTask());
        t.start();
    }
}

На первый взгляд разница минимальна. Но дьявол, как всегда, в деталях.

Топ-5 ошибок при работе с Thread и Runnable

Ошибка №1: Наследование Thread там, где нужен Runnable

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

Пример плохого кода:

public class DataProcessor extends Thread {
    // Уже нельзя extends JFrame, ArrayList или другой полезный класс
    @Override
    public void run() {
        // обработка данных
    }
}

Как надо: Используйте implements Runnable. Это интерфейс — вы можете реализовать его в любом классе, который уже наследует что-то ещё.

public class DataProcessor extends BaseService implements Runnable {
    @Override
    public void run() {
        // обработка данных
    }
}

Ошибка №2: Путаница между run() и start()

Классика жанра. Новички (и не только) часто вызывают run() вместо start(). Разница критическая: start() создаёт новый поток и внутри него вызывает run(). run() же выполняется в текущем потоке, как обычный метод.

Пример ошибки:

Thread t = new Thread(new MyTask());
t.run(); // Всё работает, но в том же потоке! Многопоточности нет.

Вывод в консоль будет в основном потоке main, а не в новом. Ошибка трудноуловима, потому что код не падает — он просто не делает то, что вы ожидаете.

Как избежать: Всегда используйте start() для запуска нового потока. Если сомневаетесь — проверьте имя потока через Thread.currentThread().getName().

Ошибка №3: Игнорирование пула потоков (ExecutorService)

Многие пишут new Thread(runnable).start() в цикле. Это работает, но крайне неэффективно. Создание потока — дорогая операция. Если у вас 1000 задач, вы создадите 1000 потоков, что приведёт к деградации производительности или даже к OutOfMemoryError.

Плохой код:

for (int i = 0; i < 1000; i++) {
    new Thread(new MyTask()).start();
}

Как надо: Используйте ExecutorService из java.util.concurrent. Пул потоков переиспользует созданные потоки.

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    executor.submit(new MyTask());
}
executor.shutdown();

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

Ошибка №4: Отсутствие обработки исключений в run()

Метод run() не может выбрасывать проверяемые исключения (checked exceptions). Да, сигнатура public void run() не содержит throws. Многие разработчики забывают это и пытаются пробросить исключение наружу.

Пример проблемы:

public class BadTask implements Runnable {
    @Override
    public void run() {
        Thread.sleep(1000); // Ошибка! Unhandled InterruptedException
    }
}

Как исправить: Оборачивайте код в try-catch внутри run(). Или используйте Callable, если нужно вернуть результат или пробросить исключение.

public class GoodTask implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // Восстанавливаем флаг прерывания
            System.err.println("Поток прерван: " + e.getMessage());
        }
    }
}

Обратите внимание: перехватив InterruptedException, мы должны восстановить флаг прерывания через Thread.currentThread().interrupt(). Это хорошая практика, чтобы вышестоящий код знал о прерывании.

Ошибка №5: Синхронизация на уровне класса Thread

Бывает, что разработчики пытаются синхронизировать доступ к данным, используя synchronized на объекте Thread. Это работает, но крайне ненадёжно и запутанно.

Плохой пример:

public class CounterThread extends Thread {
    private static int count = 0;

    @Override
    public void run() {
        synchronized (this) { // Синхронизация на самом потоке
            count++;
        }
    }
}

Почему это плохо: Если у вас 10 потоков, каждый синхронизируется на своём объекте

Блоги

Рекомендаций по книгам