Многопоточность в Java — это мощный инструмент, позволяющий вашим программам выполнять несколько задач одновременно, подобно шеф-повару, который одновременно варит суп, жарит стейк и нарезает салат. Освоив этот механизм, вы сможете создавать отзывчивые, быстрые и эффективные приложения, которые максимально используют возможности современных многоядерных процессоров. В этой статье мы не только разберем теорию, но и погрузимся в живые, рабочие примеры, которые вы сможете сразу применить в своих проектах.
Что такое поток и зачем он нужен?
Поток (Thread) — это наименьшая единица выполнения внутри процесса. Одна Java-программа (процесс) может запускать множество потоков, которые выполняются параллельно или псевдопараллельно, разделяя ресурсы этого процесса. Представьте банк с одним кассиром (однопоточность) и банк с несколькими кассирами (многопоточность). Во втором случае клиенты обслуживаются быстрее, а система в целом — эффективнее.
Ключевая идея: Многопоточность не всегда делает программу быстрее для одной задачи. Её сила — в отзывчивости (интерфейс не "зависает" при долгой операции) и в параллельной обработке множества независимых задач (например, запросов к серверу).
Способы создания потоков в Java
В Java есть два основных способа создать и запустить поток.
1. Наследование от класса Thread
Это классический, но менее гибкий способ. Вы создаете свой класс, который расширяет Thread, и переопределяете метод run().
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Поток работает: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // Важно: запускаем через start(), а не run()!
}
}
2. Реализация интерфейса Runnable
Более предпочтительный и гибкий подход, так как ваш класс может наследовать что-то ещё. Вы реализуете единственный метод run().
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Задача выполняется в потоке: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
// Или компактно с лямбдой (Java 8+):
new Thread(() -> System.out.println("Лямбда-поток!")).start();
}
}
Практические примеры многопоточности
Пример 1: Параллельная обработка элементов списка
Допустим, нам нужно применить тяжелую операцию (например, сложное вычисление или обращение к API) к каждому элементу большого списка.
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ParallelListProcessing {
public static void main(String[] args) throws InterruptedException {
List data = List.of("A", "B", "C", "D", "E");
ExecutorService executor = Executors.newFixedThreadPool(3); // Пул из 3 потоков
for (String item : data) {
executor.submit(() -> {
// Имитация долгой операции
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("Обработано: " + item + " в " + Thread.currentThread().getName());
});
}
executor.shutdown(); // Прекращаем прием новых задач
executor.awaitTermination(1, TimeUnit.MINUTES); // Ждем завершения всех задач
System.out.println("Все задачи завершены!");
}
}
Используйте пулы потоков (ExecutorService)! Создавать новый Thread для каждой задачи — дорого и неэффективно. Пул потоков переиспользует существующие потоки, управляя их жизненным циклом. Это лучшая практика в промышленной разработке.
Пример 2: Синхронизация и блокировки
Когда несколько потоков работают с общими данными, возникает состояние гонки (race condition). Для защиты используйте ключевое слово synchronized или объекты из пакета java.util.concurrent.locks.
public class SynchronizedCounter {
private int count = 0;
// Синхронизированный метод: только один поток может выполнять его за раз
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join(); // Ждем завершения t1
t2.join(); // Ждем завершения t2
System.out.println("Итоговый счетчик: " + counter.getCount()); // Всегда 2000
}
}
Пример 3: Асинхронные вычисления с Future и CompletableFuture
Современный API (Java 8+) для работы с асинхронными операциями, возвращающими результат.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class AsyncExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Запускаем долгую задачу асинхронно
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(2000); } catch (InterruptedException e) {}
return "Результат из потока";
});
System.out.println("Делаем что-то ещё в основном потоке...");
// Блокируемся и получаем результат (или обрабатываем его callback'ами)
String result = future.get();
System.out.println("Получено: " + result);
}
}
Типичные проблемы и лучшие практики
- Взаимная блокировка (Deadlock): Два или более потока бесконечно ждут друг друга. Избегайте: всегда захватывайте несколько блокировок в одном порядке.
- Гонка данных (Race Condition): Непредсказуемый результат из-за порядка выполнения. Используйте синхронизацию или атомарные классы (AtomicInteger и др.).
- Голодание (Starvation): Поток не может получить доступ к ресурсам. Настройте приоритеты и используйте честные блокировки.
- Потокобезопасные коллекции: Вместо ручной синхронизации используйте
ConcurrentHashMap,CopyOnWriteArrayListиз пакетаjava.util.concurrent.
FAQ: Часто задаваемые вопросы
В чем разница между start() и run()?
Метод run() — это просто обычный метод, который выполнит код в текущем потоке. Метод start() создает новый поток и внутри него вызывает run(). Всегда используйте start() для запуска нового потока.
Что такое демон-потоки?
Демон-потоки (фоновые потоки) автоматически завершаются, когда все обычные (пользовательские) потоки завершили работу. Установить: thread.setDaemon(true) до вызова start(). Подходят для фоновых задач, например, сборщика мусора.
Как правильно останавливать потоки?
Методы stop() и suspend() устарели и опасны. Используйте флаги или метод interrupt(), который выставляет статус прерывания. Поток должен периодически проверять Thread.interrupted() и корректно завершаться.
Когда использовать многопоточность, а когда асинхронность?
Многопоточность (Thread) — для параллельных CPU-задач (вычисления, обработка данных). Асинхронность (CompletableFuture, реактивные стеки) — для работы с I/O (сеть, файлы, БД), где потоки часто простаивают в ожидании. В реальности подходы комбинируются.