Многопоточность в Java — это не просто абстрактная концепция из учебников, а мощный инструмент для создания отзывчивых и высокопроизводительных приложений. Понимание потоков (threads) открывает двери к эффективному использованию современных многоядерных процессоров, позволяя вашим программам выполнять несколько задач одновременно. В этой статье мы разберемся, как это работает на практике, с живыми, рабочими примерами, которые вы сможете адаптировать под свои нужды.
Что такое поток и зачем он нужен?
Поток (Thread) — это наименьшая единица выполнения внутри процесса. Одно приложение (процесс) может запускать множество потоков, которые выполняются параллельно или псевдопараллельно, деля между собой ресурсы процессора. Представьте веб-сервер: один поток обрабатывает ваш запрос, другой — запрос другого пользователя, а третий в фоне чистит кэш. Без многопоточности сервер бы обрабатывал все строго по очереди, заставляя всех ждать.
Главный класс для работы с потоками в Java — java.lang.Thread. Поток можно создать двумя основными способами: наследованием от класса Thread или реализацией интерфейса Runnable.
Создание потоков: два классических способа
1. Наследование от класса Thread
Это более простой, но менее гибкий способ, так как Java не поддерживает множественное наследование.
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Поток " + getName() + ": " + i);
try {
Thread.sleep(500); // Имитация работы
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Запуск:
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start(); // Важно: запускать start(), а не run()!
thread2.start();
}
}
2. Реализация интерфейса Runnable (рекомендуется)
Это более предпочтительный подход, так как позволяет вашему классу наследоваться от другого класса.
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Поток " + Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Запуск:
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable(), "Поток-1");
Thread thread2 = new Thread(new MyRunnable(), "Поток-2");
thread1.start();
thread2.start();
}
}
Проблемы многопоточности и их решение
Когда потоки начинают работать с общими ресурсами (например, одной переменной), возникает состояние гонки (race condition).
// Проблемный пример
class Counter {
private int count = 0;
public void increment() { count++; } // Небезопасно!
public int getCount() { return count; }
}
// Потоки одновременно вызывают increment() -> результат непредсказуем.
Синхронизация с помощью synchronized
Ключевое слово synchronized гарантирует, что только один поток в один момент времени может выполнить синхронизированный метод или блок кода.
class SafeCounter {
private int count = 0;
public synchronized void increment() { count++; } // Теперь безопасно!
public synchronized int getCount() { return count; }
}
// Или синхронизация блока:
public void increment() {
synchronized(this) { // Синхронизация по объекту this
count++;
}
}
Синхронизация решает проблему согласованности, но может привести к взаимным блокировкам (deadlock) или снижению производительности. Всегда старайтесь минимизировать область синхронизированного кода.
Современный подход: пакет java.util.concurrent
Для сложных задач используйте высокоуровневые инструменты из этого пакета.
Пример с ExecutorService и Callable
import java.util.concurrent.*;
public class ConcurrentExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2); // Пул из 2 потоков
// Задача, возвращающая результат
Callable task = () -> {
Thread.sleep(1000);
return 42;
};
Future future = executor.submit(task);
System.out.println("Результат задачи: " + future.get()); // Будет ждать завершения
executor.shutdown(); // Важно: завершаем работу пула
}
}
Атомарные операции (AtomicInteger)
Для простых операций над общими переменными используйте атомарные классы. Они быстрее и безопаснее synchronized в многих сценариях.
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); } // Атомарная операция
public int getCount() { return count.get(); }
}
Практический пример: параллельная обработка списка
Допустим, нам нужно быстро обработать большой список чисел.
import java.util.Arrays;
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 numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
ExecutorService executor = Executors.newFixedThreadPool(4);
for (Integer num : numbers) {
executor.submit(() -> {
int result = num * num; // Какая-то "тяжелая" операция
System.out.println(Thread.currentThread().getName() + ": " + num + "^2 = " + result);
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES); // Ждем завершения всех задач
System.out.println("Обработка завершена!");
}
}
FAQ: Часто задаваемые вопросы о многопоточности в Java
В чем разница между start() и run()?
Метод run() содержит код для выполнения в потоке, но его прямой вызов запускает этот код в текущем (основном) потоке. Метод start() создает новый поток и внутри него вызывает run().
Что такое deadlock и как его избежать?
Взаимная блокировка (deadlock) возникает, когда два или более потока бесконечно ждут друг друга, удерживая нужные друг другу ресурсы. Для предотвращения:
- Устанавливайте порядок захвата блокировок (всегда в одной последовательности).
- Используйте
tryLock()изReentrantLockс таймаутом. - Избегайте вложенной синхронизации.
Когда использовать synchronized, а когда java.util.concurrent?
Используйте synchronized для простых случаев с низкой конкуренцией. Для сложных, высоконагруженных систем с большим количеством потоков предпочтительнее классы из java.util.concurrent (например, ConcurrentHashMap, AtomicInteger, ReentrantLock), так как они часто обеспечивают лучшую производительность и более гибкое управление.
Что такое пул потоков (ThreadPool) и зачем он нужен?
Создание потока — дорогая операция. Пул потоков (например, через Executors.newFixedThreadPool()) создает заранее заданное количество потоков и переиспользует их для выполнения множества задач. Это экономит ресурсы и позволяет контролировать максимальную нагрузку на систему.