Логотип Workflow

Article

Multithreading Basics

Тема 8. Multithreading Basics: базовые концепции многозадачности и потоков

Эта тема важна не только для Java. Многозадачность это фундамент любой современной системы: браузер, сервер, мобильное приложение, база данных, ОС. Если не понимать базовые принципы, ошибки в конкурентном коде выглядят «случайными» и плохо воспроизводятся.

Что такое многозадачность

Многозадачность это выполнение нескольких задач в перекрывающемся времени. Есть два режима:

  1. Concurrency (конкурентность): задачи чередуются во времени и продвигаются вместе.
  2. Parallelism (параллелизм): задачи реально выполняются одновременно на разных ядрах CPU.

Простая аналогия:

  • Concurrency: один повар быстро переключается между несколькими блюдами.
  • Parallelism: несколько поваров готовят одновременно.

Concurrency models

Процесс и поток

  • Process (процесс): отдельный запущенный экземпляр программы с собственной памятью.
  • Thread (поток): «линия выполнения» внутри процесса.

Один процесс может иметь много потоков, которые делят общую память процесса. Это быстро и удобно, но приводит к риску гонок, когда потоки одновременно меняют общие данные.

Как создается иллюзия одновременности

Даже на одном ядре кажется, что все идет параллельно, потому что планировщик ОС выдает потокам короткие кванты CPU (time slices).

Time slice scheduling

Поток немного поработал, затем CPU получает другой поток, и так по кругу. Переключение очень быстрое, поэтому для пользователя задачи выглядят «одновременными».

Почему многопоточность сложная

Главная сложность не в запуске потоков, а в корректности совместного доступа к данным.

Типовые проблемы:

  1. Race condition: результат зависит от случайного порядка операций.
  2. Visibility issue: один поток изменил значение, другой не увидел обновление сразу.
  3. Deadlock: потоки заблокировали друг друга и не могут продолжить.
  4. Starvation: один поток долго не получает ресурсы из-за конкуренции.
  5. Livelock: потоки активны, но не делают полезного прогресса.

Race condition на простом примере

private int counter = 0;

public void increment() {
    counter++; // неатомарная операция
}

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

Race vs lock

Подходы к синхронизации

1. synchronized

Простой встроенный механизм взаимного исключения.

private int counter = 0;

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

Что дает:

  • только один поток одновременно выполняет критическую секцию;
  • есть гарантии видимости изменений между потоками.

2. Lock (ReentrantLock)

Более гибкий вариант, чем synchronized.

private final Lock lock = new ReentrantLock();
private int counter = 0;

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

Плюсы:

  • tryLock, таймауты, более явный контроль.

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

Для простых счетчиков часто удобнее AtomicInteger.

private final AtomicInteger counter = new AtomicInteger(0);

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

Thread, Runnable и ExecutorService в Java

Thread и Runnable

Runnable task = () -> System.out.println("Work");
Thread t = new Thread(task);
t.start();

Работает, но плохо масштабируется при большом числе задач.

ExecutorService

На практике почти всегда лучше пул потоков.

ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> doWork("A"));
pool.submit(() -> doWork("B"));
pool.shutdown();

Плюсы пула:

  1. Не создаете новый поток под каждую задачу.
  2. Стабильнее нагрузка на систему.
  3. Проще управлять жизненным циклом задач.

CPU-bound и IO-bound задачи

Это ключевая концепция для выбора модели выполнения.

  • CPU-bound: упирается в вычисления (кодирование, криптография, математика).
  • IO-bound: ждет сеть, диск, БД, внешние API.

Практический эффект:

  • для CPU-bound обычно не нужен огромный пул;
  • для IO-bound часто разумно больше потоков, потому что часть времени они ждут I/O.

Безопасный дизайн конкурентного кода

  1. Минимизируйте общий изменяемый state.
  2. Предпочитайте неизменяемые объекты (immutable).
  3. Держите критические секции короткими.
  4. Используйте потокобезопасные коллекции (ConcurrentHashMap, BlockingQueue) там, где нужно.
  5. Сначала добейтесь корректности, потом оптимизируйте.

Проверка и отладка

Почему многопоточные баги «плавающие»: порядок переключения потоков недетерминирован.

Практические шаги:

  1. Писать тесты с большим числом повторов и конкурентных запусков.
  2. Логировать ключевые state-переходы и thread id.
  3. Анализировать thread dump при зависаниях.
  4. Проверять, где держатся lock'и слишком долго.

Что важно запомнить

  • Многопоточность это про корректное управление разделяемыми данными.
  • Самый надежный код часто тот, где разделяемого изменяемого состояния меньше всего.
  • Понимание базовых концепций concurrency полезно в любом языке, а не только в Java.

Quiz

Проверьте, что вы усвоили

Авторизуйтесь чтоб пройти тесты