Многопоточность в Java: создание потоков через Thread и Runnable
Для кого эта статья:
- Java-разработчики, стремящиеся улучшить навыки в многопоточности
- Студенты и начинающие программисты, изучающие язык Java
Профессионалы, готовящиеся к техническим собеседованиям в области разработки на Java
Многопоточность в Java — не просто модный термин, а необходимое условие для создания эффективных приложений. Умение управлять потоками отделяет новичка от профессионала. Представьте ресторан, где один официант обслуживает все столики: эффективность стремится к нулю, посетители недовольны. Аналогично с программами — однопоточное приложение «зависает», пользователи уходят. Я покажу, как избежать этой проблемы, шаг за шагом раскрыв секреты создания потоков в Java. Готовы увеличить производительность ваших программ в разы? Поехали! 🚀
Хотите быстро освоить многопоточность и другие продвинутые концепции Java? Курс Java-разработки от Skypro включает интенсивную практику создания параллельных приложений под руководством действующих разработчиков. Вы не просто научитесь создавать потоки — вы поймёте, как архитектурно правильно применять их в реальных проектах. Более 89% выпускников успешно решают задачи на многопоточность на технических собеседованиях!
Что такое потоки в Java и зачем их создавать
Поток (Thread) в Java представляет собой легковесный процесс — отдельную линию выполнения внутри программы. Эти линии могут работать параллельно, выполняя разные части кода одновременно. Представьте, что вы готовите сложный обед: вместо последовательного приготовления каждого блюда, вы одновременно варите суп, жарите мясо и месите тесто для десерта. Именно так работает многопоточное приложение! 💡
По умолчанию Java-программа запускается в одном потоке — main. Этого достаточно для простых задач, но для серьезных приложений такой подход быстро становится узким местом.
Александр Петров, руководитель отдела разработки
Однажды наша команда столкнулась с проблемой в приложении для обработки платежей. При высокой нагрузке интерфейс начинал "замерзать", а пользователи жаловались на долгое ожидание. Анализ показал, что все операции — обработка платежа, валидация карты и обновление интерфейса — выполнялись в одном потоке. Когда мы вынесли тяжелые операции в отдельные потоки, интерфейс оставался отзывчивым даже при обработке сотен транзакций. Время отклика сократилось с 5-7 секунд до миллисекунд, а количество успешных транзакций выросло на 42%.
Существует несколько веских причин для использования нескольких потоков:
- Повышение производительности — использование всех ядер процессора для параллельных вычислений
- Отзывчивость приложения — пользовательский интерфейс остаётся доступным при выполнении тяжёлых задач
- Эффективное использование ресурсов — пока один поток ожидает ввода/вывода, другие могут продолжать работу
- Упрощение архитектуры — разделение сложной задачи на параллельные блоки
| Характеристика | Однопоточное приложение | Многопоточное приложение |
|---|---|---|
| Использование ресурсов CPU | Одно ядро (неэффективно) | Все доступные ядра |
| Отзывчивость UI | Блокируется при тяжелых операциях | Остаётся активным |
| Сложность разработки | Низкая | Высокая |
| Типичные сценарии | Простые скрипты, утилиты | Сервера, игры, многопользовательские приложения |
Жизненный цикл потока в Java включает несколько состояний: New (создан), Runnable (готов к выполнению), Running (выполняется), Blocked/Waiting (заблокирован/ожидает) и Terminated (завершён). Понимание этих состояний критично для эффективного управления потоками.

Создание потоков через наследование класса Thread
Самый прямолинейный способ создания потока в Java — наследование от класса Thread. Этот подход интуитивно понятен новичкам, но имеет свои особенности, о которых нужно знать. Давайте рассмотрим пошаговый процесс создания потока через наследование:
- Создать класс, расширяющий
Thread - Переопределить метод
run()— здесь размещается код, который будет выполняться в отдельном потоке - Создать экземпляр этого класса
- Вызвать метод
start()для запуска потока
Вот простой пример создания потока через наследование:
public class MyThread extends Thread {
private final String message;
public MyThread(String message) {
this.message = message;
}
@Override
public void run() {
System.out.println("Поток запущен: " + message);
// Здесь размещается код, выполняемый в отдельном потоке
for (int i = 0; i < 5; i++) {
System.out.println(message + ": шаг " + i);
try {
// Имитация продолжительной операции
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Поток был прерван");
return;
}
}
System.out.println("Поток завершен: " + message);
}
public static void main(String[] args) {
System.out.println("Начало программы");
// Создание и запуск первого потока
MyThread thread1 = new MyThread("Поток 1");
thread1.start();
// Создание и запуск второго потока
MyThread thread2 = new MyThread("Поток 2");
thread2.start();
System.out.println("Потоки запущены, основная программа продолжает выполнение");
}
}
Важно понимать разницу между методами run() и start(). Если вы вызовете run() напрямую, код выполнится в текущем потоке, а не в новом. Метод start() создаёт новый поток и затем вызывает run() уже в контексте этого нового потока.
Преимущества подхода через наследование:
- Простота и наглядность кода — всё в одном классе
- Прямой доступ к методам Thread, таким как
getName(),getPriority(),isAlive() - Подходит для простых задач, где не требуется наследование от других классов
Недостатки:
- Java не поддерживает множественное наследование — если класс наследует Thread, он не может наследовать другие классы
- Тесная связь между задачей (что делать) и механизмом её выполнения (как делать)
- Менее гибкий подход для повторного использования кода
Реализация интерфейса Runnable для работы с потоками
Второй и более предпочтительный способ создания потоков — реализация интерфейса Runnable. Этот подход соответствует принципу разделения обязанностей: ваш класс определяет задачу (что делать), а класс Thread отвечает за выполнение (как делать).
Марина Соколова, тимлид Java-разработки
При работе над банковской системой мы столкнулись с необходимостью выполнять десятки различных задач параллельно: проверять транзакции, генерировать отчёты, отправлять уведомления. Сначала мы использовали наследование Thread, но быстро наткнулись на "стеклянный потолок" этого подхода. Многие наши сервисные классы уже наследовали функциональность от других классов, и добавить наследование Thread было невозможно. Переход на Runnable решил проблему элегантно: мы создали десятки специализированных задач, не меняя иерархию наследования. Производительность выросла на 30%, а объём кода сократился почти вдвое благодаря лучшей переиспользуемости компонентов.
Для создания потока с помощью Runnable следуйте этим шагам:
- Создать класс, реализующий интерфейс
Runnable - Реализовать метод
run() - Создать экземпляр этого класса
- Передать экземпляр в конструктор
Thread - Вызвать метод
start()у экземпляраThread
Вот пример создания потока с использованием Runnable:
public class MyRunnable implements Runnable {
private final String message;
public MyRunnable(String message) {
this.message = message;
}
@Override
public void run() {
System.out.println("Задача запущена: " + message);
for (int i = 0; i < 5; i++) {
System.out.println(message + ": итерация " + i);
try {
// Имитация длительной операции
Thread.sleep(700);
} catch (InterruptedException e) {
System.out.println("Задача прервана");
return;
}
}
System.out.println("Задача завершена: " + message);
}
public static void main(String[] args) {
System.out.println("Программа стартует");
// Создание задач
Runnable task1 = new MyRunnable("Задача A");
Runnable task2 = new MyRunnable("Задача B");
// Создание потоков и передача им задач
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
// Запуск потоков
thread1.start();
thread2.start();
System.out.println("Основной поток продолжает работу");
}
}
Преимущества использования Runnable:
- Разделение задачи и механизма её выполнения (соответствие принципу единственной ответственности)
- Возможность наследовать другие классы, кроме Thread
- Повторное использование логики в разных контекстах
- Возможность передавать одну и ту же задачу нескольким потокам
- Лучшая совместимость с современными API (ExecutorService, ThreadPool)
Сравнение подходов к созданию потоков:
| Критерий | Наследование Thread | Реализация Runnable |
|---|---|---|
| Возможность наследовать другие классы | Нет | Да |
| Разделение ответственности | Слабое | Сильное |
| Повторное использование кода | Ограниченное | Широкое |
| Доступ к методам Thread | Прямой | Через Thread.currentThread() |
| Совместимость с современными API | Ограниченная | Полная |
В большинстве случаев подход через Runnable предпочтительнее, особенно для сложных приложений с богатой объектной моделью. 🧩
Современные способы создания потоков с лямбда-выражениями
С появлением Java 8 создание потоков стало еще проще и элегантнее благодаря лямбда-выражениям и функциональным интерфейсам. Интерфейс Runnable является функциональным, то есть содержит только один абстрактный метод, что позволяет использовать лямбда-выражения для его реализации. 🌟
Вот как можно создать поток с использованием лямбда-выражения:
public class ModernThreads {
public static void main(String[] args) {
System.out.println("Начало программы");
// Создание потока с использованием лямбда-выражения
Thread thread1 = new Thread(() -> {
System.out.println("Поток 1 запущен");
for (int i = 0; i < 5; i++) {
System.out.println("Поток 1: итерация " + i);
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Поток 1 завершен");
});
// Ещё более компактная запись для простых задач
Thread thread2 = new Thread(() ->
System.out.println("Поток 2 выполнил быструю задачу")
);
// Запуск потоков
thread1.start();
thread2.start();
// Создание и запуск потока одной строкой
new Thread(() -> System.out.println("Поток 3 запущен и выполнен")).start();
System.out.println("Основной поток продолжает выполнение");
}
}
Для более сложных задач можно использовать ссылки на методы:
public class MethodReferenceThread {
public static void processingTask() {
System.out.println("Выполняется задача обработки данных...");
// Логика обработки данных
}
public void notificationTask() {
System.out.println("Отправка уведомлений...");
// Логика отправки уведомлений
}
public static void main(String[] args) {
// Использование ссылки на статический метод
Thread thread1 = new Thread(MethodReferenceThread::processingTask);
// Использование ссылки на метод экземпляра
MethodReferenceThread instance = new MethodReferenceThread();
Thread thread2 = new Thread(instance::notificationTask);
thread1.start();
thread2.start();
}
}
Современный Java также предлагает более высокоуровневые абстракции для работы с потоками. Один из таких инструментов — ExecutorService:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceExample {
public static void main(String[] args) {
// Создание пула потоков фиксированного размера
ExecutorService executor = Executors.newFixedThreadPool(3);
// Отправка задач на выполнение
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Задача " + taskId + " выполняется в потоке "
+ Thread.currentThread().getName());
try {
// Имитация работы
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Задача " + taskId + " завершена");
return "Результат задачи " + taskId;
});
}
// Корректное завершение executor
executor.shutdown();
try {
// Ожидание завершения всех задач (максимум 5 секунд)
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Все задачи завершены");
}
}
Основные преимущества современных подходов:
- Лаконичность кода — меньше шаблонного кода, больше бизнес-логики
- Улучшенная читаемость — намерения выражены более ясно
- Гибкость — легкость комбинирования с другими функциональными интерфейсами
- Управление ресурсами — пулы потоков обеспечивают более эффективное использование системных ресурсов
- Масштабируемость — простой переход от единичных потоков к параллельным вычислениям на уровне приложения
Помимо ExecutorService, стоит обратить внимание на CompletableFuture — мощный API для асинхронного программирования, который появился в Java 8 и был значительно улучшен в последующих версиях.
Управление потоками и лучшие практики многопоточности
Создание потоков — только начало пути. Для разработки надёжных многопоточных приложений необходимо уметь управлять потоками и соблюдать основные принципы безопасной работы с ними. Вот ключевые аспекты управления потоками: ⚙️
1. Управление жизненным циклом потока
Thread thread = new Thread(() -> {
// Код потока
});
// Запуск потока
thread.start();
// Проверка, выполняется ли поток
boolean isRunning = thread.isAlive();
// Ожидание завершения потока (блокирует текущий поток)
try {
thread.join();
// Можно указать таймаут ожидания
// thread.join(1000); // ждать максимум 1 секунду
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Прерывание потока (отмеченный как устаревший метод stop() использовать не рекомендуется)
thread.interrupt();
2. Синхронизация потоков
При работе с общими ресурсами необходимо обеспечить синхронизацию, чтобы избежать состояния гонки (race condition):
public class Counter {
private int count = 0;
// Синхронизация на уровне метода
public synchronized void increment() {
count++;
}
// Синхронизированный блок
public void decrement() {
synchronized(this) {
count--;
}
}
public int getCount() {
// Важно: даже чтение должно быть синхронизировано,
// если другие потоки могут изменять значение
synchronized(this) {
return count;
}
}
}
3. Использование Lock API
Более гибкой альтернативой synchronized является API блокировок:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private double balance;
private final Lock lock = new ReentrantLock();
public void deposit(double amount) {
lock.lock();
try {
balance += amount;
} finally {
// Обязательно освобождайте блокировку в блоке finally
lock.unlock();
}
}
public boolean withdraw(double amount) {
lock.lock();
try {
if (balance >= amount) {
balance -= amount;
return true;
}
return false;
} finally {
lock.unlock();
}
}
}
4. Обработка прерываний
Корректная обработка прерываний позволяет потокам чисто завершаться по запросу:
public void runTask() {
Thread currentThread = Thread.currentThread();
while (!currentThread.isInterrupted()) {
// Выполнение задачи
try {
// Потенциально блокирующая операция
processNextItem();
} catch (InterruptedException e) {
// Восстановление статуса прерывания
currentThread.interrupt();
System.out.println("Задача прервана, завершаем работу");
break;
}
}
// Код очистки ресурсов перед завершением
cleanup();
}
Следующие лучшие практики помогут избежать распространенных проблем с потоками:
- Минимизируйте общие мутабельные состояния — чем меньше потоки делят изменяемые данные, тем проще избежать проблем синхронизации
- Предпочитайте неизменяемые (immutable) объекты — они потокобезопасны по определению
- Используйте потокобезопасные коллекции из пакета
java.util.concurrent - Избегайте вложенной синхронизации для предотвращения взаимных блокировок (deadlock)
- Держите критические секции короткими — длительная блокировка ресурса снижает преимущества параллельного выполнения
- Предпочитайте высокоуровневые абстракции —
ExecutorService,CompletableFuture,ForkJoinPool - Документируйте потокобезопасность — явно указывайте, какие классы и методы потокобезопасны
Частые ошибки, которых следует избегать:
| Ошибка | Причина | Решение |
|---|---|---|
| Race condition | Несинхронизированный доступ к общим данным | Использование synchronized, Lock, AtomicXxx классов |
| Deadlock | Взаимная блокировка потоков | Соблюдение порядка захвата блокировок, использование tryLock с таймаутом |
| Livelock | Потоки постоянно меняют состояние, но не продвигаются | Добавление случайных задержек или переработка алгоритма синхронизации |
| Memory leak | Неосвобожденные ресурсы потоков | Правильное завершение потоков, использование try-with-resources |
| Неверное использование volatile | Ошибочное предположение, что volatile гарантирует атомарность операций | Использование AtomicXxx классов или правильная синхронизация |
Помните, что написание корректного многопоточного кода — это искусство, требующее понимания основ работы JVM и постоянной практики. Начинайте с простых паттернов, тщательно тестируйте код на конкурентные баги и постепенно усложняйте многопоточную архитектуру по мере роста опыта. 🧠
Освоив создание и управление потоками в Java, вы обладаете мощным инструментом для разработки высокопроизводительных приложений. Помните, что выбор между наследованием Thread, реализацией Runnable или использованием лямбда-выражений зависит от конкретной задачи. Следуйте лучшим практикам синхронизации и избегайте излишнего усложнения — часто самое элегантное решение оказывается и самым надёжным. Путь к мастерству многопоточного программирования лежит через постоянную практику и глубокое понимание механизмов параллельного выполнения.