Представьте: вы повар, который одновременно управляет 8 конфорками с завязанными глазами. Именно это мы делаем в многопоточности Java — только вместо подгорания блинов мы создаём волшебство производительности. Давайте добавим жару!

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

Потоки в Java подобны гиперактивным белкам — они могут взбираться на несколько деревьев (процессоров) одновременно, но без должной координации они разбросят ваши орехи (данные) повсюду. Вот как мы их укрощаем:

Создание потока: выберите своё оружие

// Подход «Я слишком крут для Thread»
Runnable рецептРамена = () -> {
    System.out.println("Кипячение воды в потоке: " + Thread.currentThread().getName());
};
// Классический способ
Thread потокШефПовара = new Thread(рецептРамена);
потокШефПовара.start();
// Метод Гордона Рамзи (ExecutorService)
ExecutorService кухня = Executors.newFixedThreadPool(4);
кухня.submit(() -> System.out.println("Профессионально нарезаем лук"));

Совет: ExecutorService — это как кухонная бригада, она предотвращает разрастание потоков лучше, чем су-шеф предотвращает чесночный запах.

Синхронизационное танго

Блокировки синхронизации — это чеснок многопоточности, необходимый при правильном использовании, но катастрофический при чрезмерном применении. Давайте приправим ситуацию:

class Кофемашина {
    private int зёрна;
    private final Lock блокировкаЗаваривания = new ReentrantLock();
    public void пополнить(int количество) {
        блокировкаЗаваривания.lock();
        try {
            зёрна += количество;
        } finally {
            блокировкаЗаваривания.unlock();
        }
    }
}
sequenceDiagram participant Поток1 participant Кофемашина participant Поток2 Поток1->>Кофемашина: блокировкаЗаваривания.lock() Кофемашина-->>Поток1: lock получен Поток2->>Кофемашина: блокировкаЗаваривания.lock() ОЖИДАЕТ Поток1->>Кофемашина: пополнить(50) Поток1->>Кофемашина: блокировкаЗаваривания.unlock() Кофемашина-->>Поток2: lock получен Поток2->>Кофемашина: пополнить(30)

Шаблоны проектирования: Гид Мишлен

1. Производитель-потребитель: Обслуживание ужина

BlockingQueue<Заказ> конвейерДляБилетов = new ArrayBlockingQueue<>(10);
// Шеф (Производитель)
Executors.newSingleThreadExecutor().submit(() -> {
    while (true) {
        Заказ заказ = принятьЗаказОтКассы();
        конвейерДляБилетов.put(заказ); // Блокируется, если заполнен
    }
});
// Повар (Потребитель)
Executors.newFixedThreadPool(3).submit(() -> {
    while (true) {
        Заказ заказ = конвейерДляБилетов.take(); // Блокируется, если пусто
        приготовитьЗаказ(заказ);
    }
});

2. Шаблон пула потоков: Кухонная бригада

ExecutorService шефПоварыБанкета = Executors.newWorkStealingPool();
List<Callable<Void>> свадебныеЗадачи = List.of(
    () -> { испечьТорт(); return null; },
    () -> { расставитьЦветы(); return null; },
    () -> { паниковатьВнутренне(); return null; }
);
шефПоварыБанкета.invokeAll(свадебныеЗадачи);

Тупики: Кошмар на кухне

В тот раз я заперся в морозильной камере (образно говоря):

// НЕ ПРОБУЙТЕ ЭТО ДОМА
Object нож = new Object();
Object разделочнаяДоска = new Object();
new Thread(() -> {
    synchronized (нож) {
        synchronized (разделочнаяДоска) { /* Нарезать овощи */ }
    }
}).start();
new Thread(() -> {
    synchronized (разделочнаяДоска) {
        synchronized (нож) { /* Беда ждёт своего часа */ }
    }
}).start();

Руководство по выживанию:

  • Всегда получайте блокировки в последовательном порядке
  • Используйте tryLock() с таймаутами
  • Делайте синхронизированные блоки короче, чем видео в TikTok

Атомное оружие (буквально)

AtomicInteger счётчикКофе = new AtomicInteger(0);
// Утренняя рутина во всех потоках
IntStream.range(0, 100)
.parallel()
.forEach(i -> счётчикКофе.accumulateAndGet(1, Math::addExact));

Советы от мудреца потоков

  1. Параллельные коллекции > Синхронизированные коллекции Зачем использовать кувалду, когда нужен скальпель? Предпочитайте ConcurrentHashMap вместо Collections.synchronizedMap().
  2. Локальное хранилище потоков Как будто у каждого потока свой набор кухонных инструментов:
    ThreadLocal<SimpleDateFormat> форматДаты = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("гггг-ММ-дд"));
    
  3. CompletableFuture Швейцарский армейский нож асинхронных операций:
    CompletableFuture.supplyAsync(this::получитьРецепт)
        .thenApply(this::подобратьИнгредиенты)
        .thenAccept(this::приготовитьБлюдо)
        .exceptionally(ex -> { записать("Подгорел чеснок!"); return null; });
    

Когда потоки выходят из-под контроля: Истории отладки

Однажды наш веб-сервер превратился в цифрового Икара (реальная история):

  • Симптом: случайные ошибки 500 во время пиковой нагрузки
  • Расследование: JStack показал 200 потоков, застрявших на HashMap.put()
  • Исправление: перешли на ConcurrentHashMap
  • Урок: общее изменяемое состояние — это Хереброн в мире многопоточности

Будущее виртуально (потоки)

Виртуальные потоки Java 21 подобны квантовым поварам — могут быть в нескольких местах одновременно без лишних затрат:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000)
        .forEach(i -> executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        }));
} // Закрывает executor автоматически

Заключительный байт

Помните, многопоточность — это как джаз, магия происходит в импровизации между структурированными частями. Теперь идите и сочините свою симфонию конкурентности! 🎻