В чём разница между Thread и Runnable
Разница между Thread и Runnable заключается в фундаментальном разделении обязанностей по паттерну «Команда» (Command Pattern): Runnable — это задача («что делать»), а Thread — э...
🟢 Junior Level
Краткий ответ для собеседования (30 секунд)
Разница между Thread и Runnable заключается в фундаментальном разделении обязанностей по паттерну «Команда» (Command Pattern): Runnable — это задача («что делать»), а Thread — это исполнитель и контекст выполнения («кто и в какой среде выполняет»).
Runnable — это функциональный интерфейс с единственным методом void run(), представляющий собой абстрактный блок исполняемого кода без привязки к конкретному потоку.
Thread — это класс среды исполнения, управляющий потоком операционной системы (или виртуальным потоком в Java 21), его состоянием, приоритетом, стеком вызовов и жизненным циклом. Наследование от Thread связывает задачу с исполнителем намертво, тогда как реализация Runnable позволяет выполнять одну и ту же задачу в пулах потоков (ExecutorService), виртуальных потоках (Virtual Threads) или даже синхронно в текущем потоке.
Сравнительная таблица: Thread vs Runnable
| Характеристика | Runnable |
Thread |
|---|---|---|
| Тип сущности | Функциональный интерфейс (@FunctionalInterface) |
Конкретный класс (java.lang.Thread) |
| Роль в архитектуре | Единица работы / Задача (Task) | Носитель выполнения / Поток (Worker/Carrier) |
| Выделение ресурсов | Обычный объект в куче (~24–32 байта) | Нативный стек ядра ОС (~1 МБ) + дескриптор OSThread |
| Способ запуска | Метод run() (выполняется в текущем потоке) |
Метод start() (порождает новый поток ОС через JNI) |
| Переиспользование | Многократное (в цикле, в пуле, на лету) | Одноразовый (повторный start() запрещен) |
| Наследование | Позволяет классу наследовать любой другой класс | Занимает единственный слот наследования Java (extends) |
| Интеграция | Стандарт для ExecutorService, Loom, Streams |
Низкоуровневый примитив, редко создается напрямую |
Примеры на Java 21: Правильный запуск и критическая ловушка
import java.util.concurrent.Executors;
public class ThreadVsRunnableDemo {
public static void main(String[] args) throws Exception {
// 1. Определение задачи через лямбду Runnable
Runnable printTask = () -> {
System.out.println("Выполнение задачи в потоке: " + Thread.currentThread().getName());
};
// ❌ ГРУБАЯ ОШИБКА: Прямой вызов run() не создает нового потока!
printTask.run(); // Выведет: "Выполнение задачи в потоке: main"
// ✅ Запуск через классический Platform Thread
Thread platformThread = new Thread(printTask, "platform-worker-1");
platformThread.start(); // Создает системный поток ОС
// ✅ Современный запуск через Virtual Thread (Java 21)
Thread virtualThread = Thread.ofVirtual()
.name("virtual-worker-1")
.start(printTask);
// ✅ Продакшен-стандарт: передача Runnable в ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(printTask);
}
platformThread.join();
virtualThread.join();
}
}
🟡 Middle Level
Принцип «Композиция вместо наследования» (Composition over Inheritance)
В устаревших учебниках часто предлагается создавать потоки через наследование: class MyTask extends Thread. В современном коммерческом коде это признано антипаттерном по трем причинам:
- Нарушение Single Responsibility Principle (SRP): класс одновременно отвечает и за бизнес-логику вычислений, и за низкоуровневое управление системным потоком.
- Лишение множественного наследования: в Java запрещено множественное наследование классов. Унаследовав
Thread, класс больше не может унаследовать доменный базовый класс (например,BaseEntityService). - Невозможность работы с пулами потоков: методы
ExecutorService.submit()принимаютRunnableилиCallable. Если задача «зашита» вThread, пул не сможет переиспользовать свои рабочие потоки для ее исполнения.
Жизненный цикл потока (Thread.State)
Объект Thread в процессе жизни проходит через строго определенные состояния, зафиксированные в java.lang.Thread.State:
[ NEW ]
│ thread.start()
▼
[ RUNNABLE ] ◄────────────────────────┐
│ │ │
Ждет замок │ │ wait() / join() / park() │ Оповещение / замок свободен
synchronized │ ▼ │
│ [ WAITING / TIMED_WAITING ] ─────┘
▼
[ BLOCKED ]
│
│ Завершение метода run()
▼
[ TERMINATED ]
NEW: объектThreadсоздан в куче, но системный поток ядра еще не выделен (методstart()не вызывался).RUNNABLE: поток выполняется на процессоре либо ожидает очереди планировщика ОС (в Java состояния Ready и Running объединены в одно).BLOCKED: поток приостановлен в ожидании захвата монитораsynchronized(не путать с ожиданием замковReentrantLock).WAITING: поток ожидает сигнала без таймаута (Object.wait(),Thread.join(),LockSupport.park()).TIMED_WAITING: ожидание с ограничением по времени (Thread.sleep(millis),Object.wait(timeout)).TERMINATED: выполнение методаrun()завершено (успешно или по исключению).
Обработка необработанных исключений: UncaughtExceptionHandler
Метод Runnable.run() не декларирует проверяемых исключений (throws Exception). Если внутри run() выбрасывается непроверяемое исключение (RuntimeException), оно прерывает выполнение потока:
public class ThreadExceptionHandlerDemo {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
throw new IllegalStateException("Критический сбой бизнес-логики!");
});
// Регистрация обработчика сбоев для конкретного потока:
worker.setUncaughtExceptionHandler((thread, throwable) -> {
System.err.println("Поток " + thread.getName() + " аварийно завершился: " + throwable.getMessage());
// Здесь отправляется алерт в мониторинг (Sentry, Prometheus)
});
// Глобальный обработчик по умолчанию для всей JVM:
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.err.println("Глобальный перехват сбоя потока: " + t.getName());
});
worker.start();
}
}
🔴 Senior Level
Under the Hood JVM HotSpot: Анатомия thread.start()
Что в точности происходит, когда вызывается метод thread.start():
Java Code: thread.start()
│
▼ (Java Native Interface - JNI)
HotSpot C++: JVM_StartThread (jvm.cpp)
│
├─► Выделение C++ структуры JavaThread
├─► Вызов os::create_thread (os_linux.cpp)
│ │
│ ▼ System Call (Linux Kernel)
│ pthread_create(..., &thread_native_entry, ...)
│ │
│ ├─► Резервирование стека (-Xss: 1 MB)
│ ├─► Аллокация ядра OSThread
│ └─► Добавление в очередь планировщика Linux CFS
│
▼ HotSpot C++
Вызов thread_native_entry()
│
▼ Обратный вызов (Java Callback)
Вызов целевого метода runnable.run()
- Проверка состояния: первым делом метод
start()проверяет внутреннее полеthreadStatus. Если оно не равно нулю (поток уже запускался), немедленно выбрасываетсяIllegalThreadStateException. Поток в Java является одноразовым конечным автоматом. - Смена состояния: после системного вызова
pthread_createоперационная система возвращает дескрипторpthread_t. Поток переходит в статусRUNNABLE, и управление передается планировщику ядра.
Приоритеты потоков (Thread.setPriority) и планировщик Linux CFS
В классе Thread есть методы setPriority(int priority) с диапазоном от 1 (MIN_PRIORITY) до 10 (MAX_PRIORITY).
[!WARNING] В современных серверных средах на Linux вызов
setPriority()практически бесполезен:
Стандартный планировщик Linux — CFS (Completely Fair Scheduler) или EEVDF (в ядрах Linux 6.6+) — оперирует концепцией «виртуального времени выполнения» (vruntime) и значениямиnice(от -20 до +19). Для того чтобы обычный непривилегированный Java-процесс мог повысить приоритет потока через системный вызовsetpriority(), требуются права суперпользователя или Linux-капабилитиCAP_SYS_NICE. В Docker/Kubernetes контейнерах эти права по умолчанию отключены, поэтому JVM молча игнорирует приоритеты, выделяя всем потокам равные кванты процессорного времени.
Thread Context ClassLoader (TCCL)
Каждый объект Thread хранит ссылку на ClassLoader, называемый Context ClassLoader:
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(customClassLoader);
Зачем он нужен Senior-разработчику:
В Java существует строгая иерархия загрузчиков классов (Bootstrap $\to$ Platform $\to$ Application). Базовые классы JDK (например, java.sql.DriverManager), находящиеся в Bootstrap ClassLoader, не могут видеть классы драйверов (например, org.postgresql.Driver), находящиеся ниже по иерархии в Application ClassLoader.
Чтобы обойти это ограничение архитектуры (Service Provider Interface, SPI), DriverManager берет загрузчик из контекста текущего потока (Thread.currentThread().getContextClassLoader()), динамически находя и загружая сторонние драйверы и плагины в рантайме.
🎯 Шпаргалка для интервью
1. 30-секундный эссенс (Elevator Pitch)
Runnable — это функциональный интерфейс абстрактной задачи («что делать»), а Thread — конкретный класс системного исполнителя («кто и на каких ресурсах выполняет»). В профессиональном коде всегда используется Runnable (или Callable), а прямое создание Thread или наследование от него (extends Thread) считается антипаттерном. Запуск потока осуществляется исключительно через start() (который через JNI вызывает системный pthread_create), тогда как прямой вызов run() просто выполнит код синхронно в текущем потоке.
2. 4 каверзных вопроса с глубокими ответами
Вопрос 1: Что произойдет на уровне JVM и операционной системы, если вызвать thread.run() вместо thread.start()?
Ответ:
Никакого нового потока операционной системы создано не будет. Метод run() выполнится как обычный синхронный вызов метода в стеке текущего потока (например, main). Системные вызовы ядра (pthread_create / CreateThread) не инициируются, нативный стек памяти не выделяется, а состояние объекта Thread останется NEW. Это распространенная ошибка новичков, приводящая к блокировке вызывающего потока.
Вопрос 2: Почему повторный вызов start() на объекте Thread выбрасывает IllegalThreadStateException, даже если поток уже завершился (TERMINATED)?
Ответ:
Жизненный цикл Thread спроектирован как необратимый конечный автомат (State Machine). В начале метода start() проверяется приватное поле threadStatus:
if (threadStatus != 0) throw new IllegalThreadStateException();
Когда поток стартует, threadStatus изменяется, а при выходе из run() переходит в статус TERMINATED. Нативный поток ядра ОС после завершения уничтожается и освобождает свой системный стек. Перезапуск того же экземпляра Java Thread нарушил бы целостность структур JNI и HotSpot.
Вопрос 3: В каком порядке JVM ищет обработчик необработанного исключения (UncaughtExceptionHandler) при сбое потока?
Ответ: JVM опрашивает обработчики по строгому каскаду:
- Индивидуальный обработчик потока:
thread.getUncaughtExceptionHandler(); - Если он не установлен, опрашивается обработчик группы потоков (
ThreadGroup), к которой принадлежит поток (по умолчанию группа делегирует поиск родителю); - Если и он отсутствует, опрашивается глобальный обработчик уровня всей JVM:
Thread.getDefaultUncaughtExceptionHandler(); - Если ни один не задан, JVM выводит стек ошибки в системный поток
System.err.
Вопрос 4: Чем отличается состояние BLOCKED от состояния WAITING в дампах потоков?
Ответ:
- В состояние
BLOCKEDпоток переходит исключительно при попытке входа в синхронизированный блок или метод (synchronized), когда ассоциированный мониторObjectMonitorуже захвачен другим потоком. - В состояние
WAITINGпоток переходит добровольно в результате вызовов методов ожидания без таймаута:Object.wait(),Thread.join()илиLockSupport.park()(на котором построены все примитивыjava.util.concurrent, включаяReentrantLockи виртуальные потоки).
3. Типичные ошибки и Red Flags
❌ “Наследование класса от Thread для реализации фоновой задачи.”
Правильно: Это связывает логику задачи с жизненным циклом потока. Следует реализовывать Runnable и передавать его в ExecutorService или Thread.startVirtualThread().
❌ “Использование thread.stop() или thread.suspend() для остановки потока.”
Правильно: Эти методы устарели (deprecated for removal) и небезопасны: они мгновенно разблокируют все мониторы, оставляя разделяемые структуры данных в поврежденном несогласованном состоянии. Остановка реализуется кооперативно через thread.interrupt().
❌ “Попытка управлять порядком выполнения критических задач через thread.setPriority().”
Правильно: Приоритеты Java игнорируются большинством серверных ядер Linux. Для приоритизации задач следует использовать специализированные очереди, например, PriorityBlockingQueue.