← Назад к вопросам

Какой принцип лежит в основе Java Memory Model?

2.7 Senior🔥 181 комментариев
#JVM и управление памятью#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Java Memory Model: основной принцип и механизмы

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

Основной принцип JMM

Happens-Before отношение — это ключевой принцип, лежащий в основе Java Memory Model.

// Смысл: если операция A happens-before операции B,
// то все результаты операции A видны потоку,
// выполняющему операцию B

int x = 0;
boolean ready = false;

// Поток 1
x = 1;          // Операция A
ready = true;   // Операция B

// Поток 2
while (!ready);  // Спин-ожидание
print(x);        // Гарантированно выведет 1, а не 0

Это не гарантируется без синхронизации:

// НЕПРАВИЛЬНО: data race!
int x = 0;
boolean ready = false;

// Поток 1
x = 1;
ready = true;

// Поток 2
while (!ready);
print(x);  // Может быть 0 или 1! Неопределено!

Правила Happens-Before

1. Внутри одного потока (Program Order)

public class SingleThreadOrder {
    public static void main(String[] args) {
        int x = 1;      // Операция 1
        int y = x + 1;  // Операция 2 happens-after Операции 1
        print(y);       // Всегда 2
    }
}

2. Volatile переменные

public class VolatileExample {
    private static volatile boolean flag = false;
    private static int value = 0;
    
    // Volatile WRITE happens-before volatile READ
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            value = 42;        // Обычная операция
            flag = true;       // Volatile WRITE
        });
        
        Thread t2 = new Thread(() -> {
            while (!flag);     // Volatile READ
            print(value);      // Гарантированно 42!
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

Использую volatile когда нужна видимость без полной синхронизации:

public class VolatileVsSync {
    // ✅ Хорошо: только флаги и flags
    private volatile boolean running = true;
    private volatile int counter = 0;
    
    // ❌ Плохо: невидимость изменений
    private boolean notVolatile = true;
    
    public void stop() {
        running = false;  // Все потоки увидят изменение
    }
}

3. Synchronized блоки / методы

public class SynchronizationHB {
    private int sharedData = 0;
    
    // Monitor EXIT happens-before Monitor ENTER
    public synchronized void writer() {
        sharedData = 42;  // Все операции внутри
    }                     // SYNCHRONIZED WRITE (monitor exit)
    
    public synchronized void reader() {
        // SYNCHRONIZED READ (monitor enter)
        print(sharedData);  // Гарантированно видит 42
    }
}

4. Запуск потока (Thread.start())

public class ThreadStartHB {
    private int value = 0;
    
    public void main(String[] args) throws InterruptedException {
        value = 42;
        
        Thread t = new Thread(() -> {
            print(value);  // Гарантированно 42!
        });
        
        t.start();  // Thread START happens-before действия в потоке
        t.join();
    }
}

5. Завершение потока (Thread.join())

public class ThreadJoinHB {
    private int value = 0;
    
    public void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            value = 42;
        });
        
        t.start();
        t.join();  // Thread TERMINATION happens-before return from join()
        
        print(value);  // Гарантированно 42!
    }
}

6. Блокировки (Lock.lock() / unlock())

public class LockHB {
    private Lock lock = new ReentrantLock();
    private int value = 0;
    
    public void writer() {
        lock.lock();
        try {
            value = 42;
        } finally {
            lock.unlock();  // UNLOCK happens-before next LOCK
        }
    }
    
    public void reader() {
        lock.lock();
        try {
            print(value);   // Гарантированно 42!
        } finally {
            lock.unlock();
        }
    }
}

Практические примеры data races

Пример 1: Опасная инициализация

// НЕПРАВИЛЬНО: Double-Checked Locking анти-паттерн
public class Singleton {
    private static Singleton instance = null;  // НЕ volatile!
    
    public static Singleton getInstance() {
        if (instance == null) {                // CHECK 1
            synchronized (Singleton.class) {
                if (instance == null) {        // CHECK 2
                    instance = new Singleton();  // Может быть не полностью инициализирован!
                }
            }
        }
        return instance;
    }
}

// ПРАВИЛЬНО: с volatile
public class SingletonFixed {
    private static volatile Singleton instance = null;  // VOLATILE!
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();  // Теперь безопасно
                }
            }
        }
        return instance;
    }
}

// ЕЩЕ ЛУЧШЕ: Lazy initialization holder
public class SingletonBest {
    private SingletonBest() {}
    
    private static class Holder {
        static final SingletonBest INSTANCE = new SingletonBest();
    }
    
    public static SingletonBest getInstance() {
        return Holder.INSTANCE;  // Классы загружаются с гарантией потокобезопасности
    }
}

Пример 2: Нескоординированные операции

// НЕПРАВИЛЬНО
public class UnsafeCounter {
    private int count = 0;  // НЕ synchronized, НЕ volatile
    
    public void increment() {
        count++;  // Data race! Может потеряться инкремент
    }
    
    public int getCount() {
        return count;  // Может вернуть старое значение
    }
}

// ПРАВИЛЬНО: вариант 1 - synchronized
public class SafeCounterSync {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

// ПРАВИЛЬНО: вариант 2 - AtomicInteger
public class SafeCounterAtomic {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // Атомарная операция
    }
    
    public int getCount() {
        return count.get();
    }
}

Memory Barriers (Barriers из процессорной архитектуры)

JMM использует память barriers для синхронизации на уровне процессора:

// Когда JVM видит volatile READ/WRITE или synchronized,
// она вставляет memory barriers:

// VOLATILE WRITE = Store Barrier
// x = 1;
// volatile_flag = true;  <- STORE BARRIER здесь
// y = 2;

// VOLATILE READ = Load Barrier
// flag_value = volatile_flag;  <- LOAD BARRIER здесь
// z = x + y;

// Это гарантирует, что процессор не переупорядочит операции

final переменные (безопасная инициализация)

// final гарантирует безопасную видимость при инициализации
public class SafeInitialization {
    private final int value;
    
    public SafeInitialization(int value) {
        this.value = value;  // FINAL WRITE happens-before конструктор завершится
    }
}

public class FinalArrayProblem {
    private final int[] array = new int[10];
    
    public void init() {
        array[0] = 1;  // Это НЕ гарантируется!
        // final гарантирует видимость ССЫЛКИ на array,
        // но НЕ видимость содержимого элементов!
    }
}

Визуализация Memory Model

Поток 1                          Поток 2
┌─────────────┐                ┌─────────────┐
│ x = 1       │                │             │
│ volatile_b  │────happens─────│ read_b      │
│ = true      │    before      │ print(x)    │
└─────────────┘                └─────────────┘
   ↓ WRITE                        ↑ READ
   BARRIER                        BARRIER

Гарантия: print(x) выведет 1, не 0

Практические рекомендации

  1. Избегай data races: используй synchronized, volatile, или AtomicXxx
  2. Volatile для флагов: простых булевых и целых для flagging
  3. Synchronized для критических секций: когда нужна атомарность
  4. AtomicXxx для счетчиков: если только нужны atomic операции
  5. Immutable объекты: если нет shared state, нет проблем
  6. final для инициализации: гарантирует видимость ссылок
// Правильный паттерн многопоточности
public class ThreadSafeService {
    private final Object lock = new Object();  // final lock
    private int sharedData = 0;  // Защищен lock
    private volatile boolean flag = false;  // Простой флаг
    private final AtomicInteger counter = new AtomicInteger();  // Атомарный счетчик
    
    public void updateData(int value) {
        synchronized (lock) {  // happens-before для всех операций внутри
            sharedData = value;
        }
    }
    
    public int getData() {
        synchronized (lock) {
            return sharedData;
        }
    }
}

Итого

Java Memory Model гарантирует: if операция A happens-before операции B, то все эффекты A видны B. Это достигается через:

  • Program Order (в одном потоке)
  • Volatile операции (READ/WRITE синхронизация)
  • Synchronized блоки (Monitor ENTER/EXIT)
  • Thread операции (start/join)
  • Lock операции (lock/unlock)

Без понимания JMM легко создать data races, которые проявляются непредсказуемо.