Статьи

Полный курс по языку Java — от установки JDK до многопоточности и HTTP-запросов

Основы многопоточности

Страница 29 из 30 · 2026-10-25 · 10 мин чтения

Зачем нужны потоки

Поток — независимая последовательность выполнения внутри процесса. Несколько потоков позволяют:

  • использовать все ядра процессора;
  • не блокировать интерфейс или сервер во время долгих операций;
  • обрабатывать много запросов одновременно.

Потоки разделяют общую память. Отсюда главная сложность: одновременный доступ к одним данным.

Создание потока

public class ThreadBasics {    public static void main(String[] args) throws InterruptedException {        Runnable task = () -> {            String name = Thread.currentThread().getName();            for (int i = 1; i <= 3; i++) {                System.out.println(name + ": шаг " + i);            }        };        Thread a = new Thread(task, "Поток-A");        Thread b = new Thread(task, "Поток-B");        a.start();        b.start();        a.join();        b.join();        System.out.println("Оба потока завершены");    }}
  • start() запускает новый поток. Вызов run() напрямую выполнит код в текущем потоке.
  • join() ждет завершения потока.
  • Порядок вывода потоков при каждом запуске может отличаться.

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

public class Worker extends Thread {    @Override    public void run() {        System.out.println("Работает " + getName());    }    public static void main(String[] args) {        new Worker().start();    }}

Предпочтительнее передавать Runnable: так задача отделена от механизма запуска.

Sleep и прерывание

public class Interrupts {    public static void main(String[] args) throws InterruptedException {        Thread worker = new Thread(() -> {            try {                while (true) {                    System.out.println("работаю");                    Thread.sleep(500);                }            } catch (InterruptedException e) {                System.out.println("прерван");            }        });        worker.start();        Thread.sleep(1600);        worker.interrupt();        worker.join();    }}

interrupt() — запрос на остановку. Поток должен сам обработать InterruptedException или проверять Thread.currentThread().isInterrupted(). Не проглатывайте исключение молча: либо завершайте работу, либо восстанавливайте флаг Thread.currentThread().interrupt().

Состояние гонки

public class RaceCondition {    static int counter = 0;    public static void main(String[] args) throws InterruptedException {        Runnable task = () -> {            for (int i = 0; i < 100_000; i++) {                counter++;            }        };        Thread a = new Thread(task);        Thread b = new Thread(task);        a.start();        b.start();        a.join();        b.join();        System.out.println(counter);    }}

Ожидается 200000, но результат почти всегда меньше. Операция counter++ состоит из трех шагов: чтение, увеличение, запись. Потоки перемешивают эти шаги и теряют обновления.

synchronized

Блок или метод synchronized может выполнять только один поток одновременно для одного монитора:

public class SafeCounter {    private int counter = 0;    public synchronized void increment() {        counter++;    }    public synchronized int get() {        return counter;    }    public static void main(String[] args) throws InterruptedException {        SafeCounter safe = new SafeCounter();        Runnable task = () -> {            for (int i = 0; i < 100_000; i++) {                safe.increment();            }        };        Thread a = new Thread(task);        Thread b = new Thread(task);        a.start();        b.start();        a.join();        b.join();        System.out.println(safe.get());    }}

Теперь результат всегда 200000.

Синхронизация блока с явным объектом-монитором:

public class Account {    private final Object lock = new Object();    private double balance;    public void deposit(double amount) {        synchronized (lock) {            balance += amount;        }    }    public double getBalance() {        synchronized (lock) {            return balance;        }    }}

Синхронизация одновременно решает две задачи: взаимное исключение и видимость изменений между потоками.

volatile

Модификатор volatile гарантирует видимость записи другими потоками, но не атомарность составных операций:

public class StopFlag {    private volatile boolean running = true;    public void stop() {        running = false;    }    public void runLoop() {        while (running) {            Thread.onSpinWait();        }        System.out.println("Остановлено");    }    public static void main(String[] args) throws InterruptedException {        StopFlag flag = new StopFlag();        Thread t = new Thread(flag::runLoop);        t.start();        Thread.sleep(200);        flag.stop();        t.join();    }}

Без volatile поток мог бы никогда не увидеть изменение флага.

Атомарные типы

Для простых счетчиков не нужна блокировка:

import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.atomic.AtomicLong;public class AtomicDemo {    public static void main(String[] args) throws InterruptedException {        AtomicInteger counter = new AtomicInteger();        Runnable task = () -> {            for (int i = 0; i < 100_000; i++) {                counter.incrementAndGet();            }        };        Thread a = new Thread(task);        Thread b = new Thread(task);        a.start();        b.start();        a.join();        b.join();        System.out.println(counter.get());        AtomicLong value = new AtomicLong(5);        System.out.println(value.compareAndSet(5, 10));        System.out.println(value.get());        System.out.println(value.updateAndGet(v -> v * 2));    }}

Блокировки Lock

import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class LockDemo {    private final Lock lock = new ReentrantLock();    private int value;    public void increment() {        lock.lock();        try {            value++;        } finally {            lock.unlock();        }    }    public boolean tryIncrement() {        if (lock.tryLock()) {            try {                value++;                return true;            } finally {                lock.unlock();            }        }        return false;    }    public int get() {        lock.lock();        try {            return value;        } finally {            lock.unlock();        }    }}

unlock() обязательно вызывать в finally.

Взаимная блокировка

Deadlock возникает, когда потоки ждут друг друга бесконечно:

public class Deadlock {    static final Object A = new Object();    static final Object B = new Object();    public static void main(String[] args) {        new Thread(() -> {            synchronized (A) {                sleep();                synchronized (B) {                    System.out.println("поток 1");                }            }        }).start();        new Thread(() -> {            synchronized (B) {                sleep();                synchronized (A) {                    System.out.println("поток 2");                }            }        }).start();    }    static void sleep() {        try {            Thread.sleep(100);        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        }    }}

Программа зависнет. Как избежать: всегда захватывать блокировки в одном и том же порядке, использовать tryLock с таймаутом, держать блокировки как можно короче. Для диагностики зависшей программы используйте jstack <pid>.

Пул потоков и ExecutorService

Создавать поток на каждую задачу дорого. Пул переиспользует потоки:

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;public class ExecutorDemo {    public static void main(String[] args) throws InterruptedException {        ExecutorService pool = Executors.newFixedThreadPool(3);        for (int i = 1; i <= 6; i++) {            int id = i;            pool.submit(() -> {                System.out.println("Задача " + id + " в " + Thread.currentThread().getName());                try {                    Thread.sleep(200);                } catch (InterruptedException e) {                    Thread.currentThread().interrupt();                }            });        }        pool.shutdown();        if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {            pool.shutdownNow();        }    }}

Пул обязательно нужно останавливать (shutdown), иначе программа не завершится.

Типы пулов:

Фабричный методНазначение
newFixedThreadPool(n)фиксированное число потоков
newCachedThreadPool()создает потоки по необходимости
newSingleThreadExecutor()один поток, задачи по очереди
newScheduledThreadPool(n)задачи по расписанию
newVirtualThreadPerTaskExecutor()виртуальный поток на задачу (Java 21)

С Java 19 ExecutorService реализует AutoCloseable, поэтому можно писать так:

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ExecutorTry {    public static void main(String[] args) {        try (ExecutorService pool = Executors.newFixedThreadPool(2)) {            pool.submit(() -> System.out.println("задача 1"));            pool.submit(() -> System.out.println("задача 2"));        }    }}

Выход из блока try ждет завершения задач.

Callable и Future

Runnable ничего не возвращает. Callable<T> возвращает результат и может бросать исключения:

import java.util.ArrayList;import java.util.List;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;import java.util.concurrent.TimeoutException;public class FutureDemo {    static long sumRange(int from, int to) {        long sum = 0;        for (int i = from; i <= to; i++) {            sum += i;        }        return sum;    }    public static void main(String[] args) throws InterruptedException, ExecutionException {        ExecutorService pool = Executors.newFixedThreadPool(4);        List<Future<Long>> futures = new ArrayList<>();        for (int part = 0; part < 4; part++) {            int from = part * 250_000 + 1;            int to = (part + 1) * 250_000;            Callable<Long> task = () -> sumRange(from, to);            futures.add(pool.submit(task));        }        long total = 0;        for (Future<Long> future : futures) {            total += future.get();        }        System.out.println(total);        Future<String> slow = pool.submit(() -> {            Thread.sleep(2000);            return "готово";        });        try {            System.out.println(slow.get(500, TimeUnit.MILLISECONDS));        } catch (TimeoutException e) {            System.out.println("Таймаут");            slow.cancel(true);        }        pool.shutdown();    }}

Метод get() блокирует вызывающий поток до получения результата. Исключение из задачи приходит как ExecutionException.

CompletableFuture

Позволяет строить асинхронные цепочки без блокировки:

import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableDemo {    static String loadUser(int id) {        sleep(300);        return "Пользователь-" + id;    }    static int loadScore(String user) {        sleep(300);        return user.length() * 10;    }    static void sleep(long ms) {        try {            Thread.sleep(ms);        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        }    }    public static void main(String[] args) throws ExecutionException, InterruptedException {        CompletableFuture<Integer> pipeline = CompletableFuture            .supplyAsync(() -> loadUser(7))            .thenApply(user -> user.toUpperCase())            .thenApply(CompletableDemo::loadScore);        System.out.println(pipeline.get());        CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> loadUser(1));        CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> loadUser(2));        CompletableFuture<String> both = a.thenCombine(b, (x, y) -> x + " и " + y);        System.out.println(both.join());        CompletableFuture<Integer> failing = CompletableFuture            .supplyAsync(() -> {                if (true) {                    throw new IllegalStateException("сбой");                }                return 1;            })            .exceptionally(e -> -1);        System.out.println(failing.join());        CompletableFuture<Void> all = CompletableFuture.allOf(a, b);        all.join();        System.out.println("Все завершены");    }}

Методы:

  • supplyAsync, runAsync — запуск асинхронной задачи;
  • thenApply — преобразовать результат;
  • thenAccept — потребить результат;
  • thenCompose — продолжить другой асинхронной задачей;
  • thenCombine — объединить два результата;
  • exceptionally, handle — обработка ошибок;
  • allOf, anyOf — ожидание нескольких.

По умолчанию задачи выполняются в общем пуле ForkJoinPool.commonPool(). Для блокирующих операций передавайте собственный Executor вторым аргументом.

Потокобезопасные коллекции

Обычные ArrayList и HashMap не потокобезопасны. Используйте:

import java.util.List;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.CopyOnWriteArrayList;public class ConcurrentCollections {    public static void main(String[] args) throws InterruptedException {        Map<String, Integer> counts = new ConcurrentHashMap<>();        List<String> log = new CopyOnWriteArrayList<>();        Runnable task = () -> {            for (int i = 0; i < 1000; i++) {                counts.merge("key", 1, Integer::sum);            }            log.add(Thread.currentThread().getName());        };        Thread a = new Thread(task);        Thread b = new Thread(task);        a.start();        b.start();        a.join();        b.join();        System.out.println(counts.get("key"));        System.out.println(log.size());    }}

Очередь для передачи данных между потоками:

import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;public class ProducerConsumer {    public static void main(String[] args) throws InterruptedException {        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);        Thread producer = new Thread(() -> {            try {                for (int i = 1; i <= 10; i++) {                    queue.put(i);                    System.out.println("Произведено " + i);                }                queue.put(-1);            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        });        Thread consumer = new Thread(() -> {            try {                while (true) {                    int value = queue.take();                    if (value == -1) {                        break;                    }                    System.out.println("Потреблено " + value);                    Thread.sleep(50);                }            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        });        producer.start();        consumer.start();        producer.join();        consumer.join();    }}

put блокируется при заполненной очереди, take — при пустой.

Синхронизаторы

import java.util.concurrent.CountDownLatch;public class LatchDemo {    public static void main(String[] args) throws InterruptedException {        int workers = 3;        CountDownLatch latch = new CountDownLatch(workers);        for (int i = 1; i <= workers; i++) {            int id = i;            new Thread(() -> {                System.out.println("Работник " + id + " закончил");                latch.countDown();            }).start();        }        latch.await();        System.out.println("Все работники закончили");    }}

Другие классы: Semaphore (ограничение числа одновременных доступов), CyclicBarrier (ожидание всех участников в точке), Phaser.

Виртуальные потоки

В Java 21 появились виртуальные потоки: очень легкие потоки, которыми управляет JVM. Они удобны для большого числа блокирующих задач (сеть, база):

import java.time.Duration;import java.util.concurrent.Executors;import java.util.stream.IntStream;public class VirtualThreads {    public static void main(String[] args) {        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {            IntStream.range(0, 10_000).forEach(i -> executor.submit(() -> {                Thread.sleep(Duration.ofSeconds(1));                return i;            }));        }        System.out.println("Готово");    }}

Одиночный виртуальный поток:

Thread.startVirtualThread(() -> System.out.println("виртуальный поток"));

10 000 обычных потоков уничтожили бы память, а виртуальные справляются без проблем. Пулировать виртуальные потоки не нужно.

Практические правила

  • Если можно, избегайте общего изменяемого состояния: используйте неизменяемые объекты.
  • Предпочитайте высокоуровневые средства (ExecutorService, CompletableFuture, ConcurrentHashMap) ручной работе с Thread и synchronized.
  • Всегда останавливайте пулы потоков.
  • Не игнорируйте InterruptedException.
  • Держите критические секции короткими, не вызывайте внешний код внутри synchronized.
  • Тестируйте многопоточный код многократно: ошибки гонки проявляются не всегда.
  • Параллельность не всегда ускоряет программу: накладные расходы могут перевесить выгоду.