Конкурентность и многопоточность в Java: от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в конкурентность и многопоточность в Java

Современные приложения всё чаще требуют одновременного выполнения нескольких задач. Будь то обработка запросов на сервере, загрузка файлов или сложные вычисления — без механизмов конкурентности и многопоточности не обойтись. В Java эти концепции заложены на уровне языка и стандартной библиотеки, что делает его одним из лучших инструментов для создания производительных приложений.

В этой статье мы разберём всё от основ до продвинутых инструментов: как создавать потоки, синхронизировать доступ к данным и использовать современные утилиты из пакета java.util.concurrent. Материал будет полезен как новичкам, так и разработчикам, желающим систематизировать знания.

Что такое поток (Thread) и зачем он нужен?

Поток — это лёгкий процесс, который выполняется внутри приложения. Каждая Java-программа имеет как минимум один поток (main). Многопоточность позволяет выполнять несколько потоков одновременно, что особенно важно на многоядерных процессорах.

Основные преимущества многопоточности:

  • Повышение производительности за счёт параллельного выполнения задач.
  • Улучшение отзывчивости пользовательского интерфейса.
  • Эффективное использование ресурсов системы.

1. Создание потоков в Java: классические подходы

В Java существует два базовых способа создания потока: наследование от класса Thread и реализация интерфейса Runnable. Рассмотрим оба.

Наследование от класса Thread

Самый простой способ — создать подкласс Thread и переопределить метод run().

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Поток " + getName() + " запущен.");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // Запускаем поток
    }
}

Важно: вызывать start(), а не run(). run() выполнит код в текущем потоке, а start() создаёт новый.

Реализация интерфейса Runnable

Более гибкий подход — реализовать Runnable и передать его в конструктор Thread.

public class Task implements Runnable {
    @Override
    public void run() {
        System.out.println("Задача выполняется в потоке: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Task task = new Task();
        Thread thread = new Thread(task);
        thread.start();
    }
}

Этот способ предпочтительнее, так как не требует наследования и позволяет классу расширять другие классы.

2. Синхронизация и атомарность: как избежать гонки данных

Когда несколько потоков одновременно обращаются к общим данным, возникает состояние гонки (race condition). Чтобы этого избежать, Java предоставляет механизмы синхронизации.

Ключевое слово synchronized

С помощью synchronized можно блокировать метод или блок кода для одного потока.

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

В этом примере только один поток может выполнять increment() в конкретный момент. Однако для простых операций, таких как инкремент, лучше использовать атомарные классы.

Атомарные классы (Atomic)

Пакет java.util.concurrent.atomic содержит классы, которые обеспечивают атомарные операции без блокировок.

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

Это быстрее и безопаснее, чем synchronized, особенно при высокой конкуренции.

3. Современный подход: ExecutorService и пулы потоков

Создавать потоки вручную для каждой задачи — плохая практика. Намного эффективнее использовать пул потоков через ExecutorService. Это часть Java Concurrency API.

Базовый пример с ExecutorService

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                System.out.println("Поток: " + Thread.currentThread().getName());
            });
        }

        executor.shutdown(); // Останавливаем пул после выполнения всех задач
    }
}

Преимущества пула потоков:

  • Ограничение количества одновременно работающих потоков.
  • Переиспользование потоков (снижение накладных расходов).
  • Удобное управление жизненным циклом.

Callable и Future: получение результата

Если задача должна вернуть результат, используйте Callable и Future.

import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Future<Integer> future = executor.submit(() -> {
            Thread.sleep(1000);
            return 42;
        });

        System.out.println("Результат: " + future.get()); // Ожидает завершения
        executor.shutdown();
    }
}

4. Продвинутые механизмы: Lock, Semaphore и CountDownLatch

Для более сложных сценариев Java предлагает дополнительные инструменты синхронизации.

Lock и ReentrantLock

Интерфейс Lock даёт больше контроля, чем synchronized: можно пробовать захватить блокировку с таймаутом или опрашивать её состояние.

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int counter = 0;

    public void safeIncrement() {
        lock.lock();
        try {
            counter++;
        } finally {
            lock.unlock();
        }
    }
}

Всегда помещайте unlock() в блок finally, чтобы избежать deadlock.

Semaphore: ограничение доступа к ресурсу

Семафор позволяет ограничить количество потоков, одновременно работающих с ресур

Блоги

Рекомендаций по книгам