В чём разница между Thread и Runnable?
Thread и Runnable — это два фундаментально разных понятия в Java, которые часто путают. Runnable описывает задачу ("что делать"), а Thread — исполнитель ("кто выполняет"). Это р...
Thread и Runnable — это два фундаментально разных понятия в Java, которые часто путают. Runnable описывает задачу (“что делать”), а Thread — исполнитель (“кто выполняет”). Это разделение — пример паттерна Command: объект-команда (Runnable) отдельно от объекта-исполнителя (Thread).
Почему это разделение важно: если бы задача была привязана к исполнителю (как при наследовании от Thread), вы не могли бы переиспользовать одну задачу в разных контекстах (пул потоков, Virtual Thread, текущий поток). Разделение даёт гибкость.
Junior уровень
Базовое понимание
Thread и Runnable — это два разных понятия в Java:
| Понятие | Что это | Роль |
|---|---|---|
| Runnable | Интерфейс (interface) |
Задача — “что делать” |
| Thread | Класс (class) |
Носитель — “кто выполняет” |
Runnable — это просто блок кода, который можно выполнить. Сам по себе он не создаёт поток. Thread — это обёртка над потоком операционной системы, которая берёт Runnable и запускает его в отдельном потоке выполнения.
Простая аналогия
- Runnable = рецепт (описание что приготовить)
- Thread = повар (кто готовит по рецепту)
Пример Runnable
// Runnable — это просто задача
Runnable task = () -> {
System.out.println("Выполняю задачу!");
};
Пример Thread
// Thread — это исполнитель
Thread thread = new Thread(task); // Передаём задачу
thread.start(); // Запускаем новый поток
Полное сравнение
| Характеристика | Runnable | Thread |
|---|---|---|
| Тип | Интерфейс | Класс |
| Метод | run() |
start(), join(), interrupt(), … |
| Создаёт поток? | Нет | Да (при вызове start()) |
| Память | Объект в куче (~24 байта) | Объект + нативный стек (~1MB) |
| Можно переиспользовать? | Да | Нет (один Thread = одно выполнение) |
Частая ошибка: run() vs start()
Runnable task = () -> System.out.println("Поток: " + Thread.currentThread().getName());
// ПЛОХО: run() — выполняется в ТЕКУЩЕМ потоке
task.run(); // "Поток: main" — НЕ новый поток!
// ХОРОШО: start() — создаёт НОВЫЙ поток
Thread thread = new Thread(task);
thread.start(); // "Поток: Thread-0" — новый поток!
Middle уровень
Composition over Inheritance
// ПЛОХО: наследование от Thread
class MyTask extends Thread {
@Override
public void run() {
// Теперь MyTask НЕ может наследовать ничего другого
}
}
// ХОРОШО: реализация Runnable
class MyTask implements Runnable {
@Override
public void run() {
// MyTask может ещё и наследовать другой класс
}
}
Почему Runnable лучше?
| Критерий | extends Thread | implements Runnable |
|---|---|---|
| Множественное наследование | Нет (занято Thread) | Да |
| Переиспользование | Нет (один Thread) | Да (в любой ExecutorService) |
| Разделение ответственности | Нет (задача + носитель) | Да (задача отдельно) |
Передача Runnable в разные исполнители
Runnable task = () -> System.out.println("Working...");
// 1. В Thread
new Thread(task).start();
// 2. В ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(task);
// 3. В ForkJoinPool
ForkJoinPool.commonPool().execute(task);
// 4. Просто в текущем потоке
task.run();
Состав Thread (что внутри)
Thread thread = new Thread(task);
// Thread содержит:
thread.getId(); // Уникальный ID
thread.getName(); // Имя ("Thread-0")
thread.getPriority(); // Приоритет (1-10)
thread.isDaemon(); // Демон или нет
thread.getContextClassLoader(); // ClassLoader для фреймворков
thread.getState(); // NEW, RUNNABLE, BLOCKED, WAITING, ...
Thread States
| Состояние | Описание |
|---|---|
| NEW | Создан, но start() не вызван |
| RUNNABLE | Выполняется или готов к выполнению |
| BLOCKED | Ждёт освобождения монитора (synchronized) |
| WAITING | Ждёт notify() или park() |
| TIMED_WAITING | Ждёт с таймаутом (sleep, wait(timeout)) |
| TERMINATED | Завершён |
Thread t = new Thread(() -> {});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (или TERMINATED если быстрый)
t.join();
System.out.println(t.getState()); // TERMINATED
Senior уровень
Under the Hood: Создание потока
// При вызове start():
thread.start();
// JVM делает:
// 1. Вызывает нативный метод:
// - Linux: pthread_create()
// - Windows: CreateThread()
// 2. ОС выделяет:
// - Stack (по умолчанию 1MB)
// - Thread-local storage
// - Записи в планировщике (Scheduler)
// 3. Добавляет в очередь планировщика ОС
// 4. Только потом вызывает Runnable.run()
Stack Memory
1000 объектов Runnable: ~24 KB (просто объекты в Young Gen)
1000 объектов Thread: ~1 GB (1000 × 1MB нативных стеков)
Thread Context ClassLoader
// Фреймворки используют для загрузки классов
Thread.currentThread().setContextClassLoader(myClassLoader);
// Hibernate, Spring и другие используют:
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class<?> clazz = cl.loadClass("com.example.MyEntity");
Priority — миф и реальность
Thread t = new Thread(task);
t.setPriority(Thread.MAX_PRIORITY); // 10
// На современных ОС (Linux CFS) приоритеты Java
// чаще всего ИГНОРИРУЮТСЯ планировщиком!
// ОС использует свои сложные алгоритмы честности
UncaughtExceptionHandler
Thread t = new Thread(() -> {
throw new RuntimeException("Непойманная ошибка!");
});
// Без обработчика — ошибка просто в System.err
t.setUncaughtExceptionHandler((thread, ex) -> {
System.err.println("Поток " + thread.getName() + " упал: " + ex);
// Логирование, алерт, restart
});
t.start();
Thread Groups — устаревшая концепция
// ThreadGroup — НЕ рекомендуется использовать
ThreadGroup group = new ThreadGroup("my-group");
new Thread(group, task).start();
// Сейчас для группировки используют:
// 1. ExecutorService
// 2. StructuredTaskScope (Java 21+)
// 3. Кастомный ThreadFactory с именами
Диагностика
jstack
jstack <pid>
"main" #1 prio=5 os_prio=0 tid=0x00007f... nid=0x1234 runnable
at com.example.MyClass.myMethod(MyClass.java:42)
"worker-1" #10 prio=5 os_prio=0 tid=0x00007f... nid=0x1235 waiting on condition
- parking to wait for <0x000000076af0c8d0>
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
Thread Dumps — что видно
Thread— видно состояние, имя, стек-трейсRunnable— НЕ видно отдельно, он “растворяется” внутри кадра стека методаrun()
Best Practices
- Реализуйте Runnable — не наследуйте Thread
- Используйте ExecutorService — не создавайте Thread напрямую
- Вызывайте start() — не run() для запуска потока
- Устанавливайте UncaughtExceptionHandler — для обработки ошибок
- Именуйте потоки — кастомный ThreadFactory для отладки
- Thread Context ClassLoader — для динамической загрузки классов
- Не используйте ThreadGroup — ExecutorService или StructuredTaskScope
- Не надейтесь на приоритеты — ОС их игнорирует
Когда НЕ нужно использовать Runnable
- Однопоточная задача — если код выполняется только в одном потоке, Runnable добавляет лишнюю абстракцию без пользы
- Нужен результат — используйте
Callable<V>вместоRunnable(Runnable.run() возвращает void) - Нужно бросить checked exception — Runnable.run() не declares throws, используйте Callable
Когда НЕ нужно создавать Thread напрямую
- Есть ExecutorService — пул потоков эффективнее (переиспользует потоки, управляет очередью)
- Virtual Threads доступны (Java 21+) — для I/O-bound задач
Thread.ofVirtual()проще и легче - StructuredTaskScope (Java 21 Preview) — для групп связанных задач с общим жизненным циклом
Runnable vs Callable vs Thread: что выбрать?
| Ситуация | Выбор | Почему |
|---|---|---|
| Задача без результата (лог, отправка) | Runnable | Простой функциональный интерфейс |
| Задача с результатом | Callable |
Возвращает V, может бросить checked exception |
| Быстрый прототип (1-2 потока) | new Thread(runnable) | Минимум кода |
| Production (пул, управление) | ExecutorService.submit(runnable) | Переиспользование потоков, очередь |
| I/O-bound, много задач (Java 21+) | Thread.ofVirtual().start(runnable) | Миллионы лёгких потоков |
🎯 Шпаргалка для интервью
Обязательно знать:
- Runnable — интерфейс (задача, “что делать”), Thread — класс (исполнитель, “кто выполняет”)
- Runnable.run() выполняется в ТЕКУЩЕМ потоке, Thread.start() создаёт НОВЫЙ поток
- Composition over Inheritance: implements Runnable лучше extends Thread (гибкость, пул потоков, Virtual Threads)
- 1000 Runnable объектов = ~24 KB, 1000 Thread = ~1 GB (нативные стеки)
- Thread States: NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
- Thread priority на современных ОС (Linux CFS) чаще всего игнорируется планировщиком
Частые уточняющие вопросы:
- Почему нельзя наследовать Thread? — Можно, но это антипаттерн: теряете множественное наследование, невозможно переиспользовать в пуле потоков
- Зачем нужен Context ClassLoader в Thread? — Фреймворки (Hibernate, Spring) используют для динамической загрузки классов из правильного classpath
- Почему ThreadGroup устарел? — Вместо него используют ExecutorService, StructuredTaskScope (Java 21+), или кастомный ThreadFactory
- Что делать с непойманными исключениями в Thread? —
setUncaughtExceptionHandler()— иначе ошибка только в System.err
Красные флаги (НЕ говорить):
- ❌ “Thread и Runnable — это одно и то же” — Runnable это задача, Thread это исполнитель
- ❌ “Вызов run() запускает новый поток” — run() выполняется в текущем потоке, нужен start()
- ❌ “Thread priority гарантирует порядок выполнения” — современные ОС игнорируют Java-приоритеты
- ❌ “ThreadGroup — это хороший способ группировки” — устарел, используйте ExecutorService или StructuredTaskScope
Связанные темы:
- [[19. Какие условия необходимы для возникновения deadlock]]
- [[21. Что такое race condition]]
- [[23. Что такое Virtual Threads в Java 21]]
- [[28. Что такое Callable и Future]]