Какова роль Java Memory Model (JMM) в многопоточном программировании?

«Какова роль Java Memory Model (JMM) в многопоточном программировании?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Java Memory Model (JMM) — это формальная спецификация, которая определяет, как потоки взаимодействуют через общую память. Она гарантирует предсказуемость поведения программы в многопроцессорных и многопоточных средах, где компилятор и процессор могут переупорядочивать инструкции для оптимизации.

Основные проблемы, которые решает JMM:

  • Видимость изменений: Изменение переменной, сделанное в одном потоке, может быть не сразу видно другому потоку из-за кешей процессора.
  • Переупорядочивание операций: Компилятор и CPU могут менять порядок независимых инструкций, что может привести к неожиданным результатам.

Ключевые концепции JMM:

  1. Happens-before: Отношение порядка между операциями. Если операция A happens-before операции B, то все изменения памяти, сделанные в A, гарантированно видны для B.
  2. Синхронизированные блоки (synchronized): Создают отношения happens-before. Вход в монитор (начало блока) happens-before всему последующему входу в тот же монитор.
  3. volatile переменные: Чтение volatile-поля happens-before любого последующего чтения того же поля. Запись в volatile-поле happens-before любого последующего чтения. Гарантирует видимость изменений и запрещает переупорядочивание операций с volatile-доступами.
  4. Запуск и завершение потоков: Запуск потока (Thread.start()) happens-before первым действием в этом потоке. Завершение потока happens-before успешному возврату из Thread.join().

Пример проблемы (гонка данных) и решение:

// ПРОБЛЕМА: Гонка данных (Race Condition)
class UnsafeCounter {
    private int count = 0; // Не volatile, не synchronized
    public void increment() { count++; } // Неатомарная операция!
    public int getCount() { return count; }
}
// В многопоточной среде два потока могут одновременно прочитать старое значение,
// увеличить его и записать обратно, потеряв одно из увеличений.

// РЕШЕНИЕ 1: Использование synchronized (создает happens-before)
class SafeCounter1 {
    private int count = 0;
    public synchronized void increment() { count++; }
    public synchronized int getCount() { return count; }
}

// РЕШЕНИЕ 2: Использование AtomicInteger (использует low-level CAS)
import java.util.concurrent.atomic.AtomicInteger;
class SafeCounter2 {
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() { count.incrementAndGet(); }
    public int getCount() { return count.get(); }
}

// РЕШЕНИЕ 3: Использование volatile (только если операция атомарна сама по себе)
class SafeFlag {
    private volatile boolean stopped = false; // Запись и чтение атомарны для boolean
    public void stop() { stopped = true; }
    public boolean isStopped() { return stopped; }
}

Без понимания JMM невозможно писать корректные и эффективные многопоточные приложения.