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

По какому принципу потоки считывают переменную в общей памяти

3.0 Senior🔥 211 комментариев
#Многопоточность

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

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

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

Memory Model в Java: Как потоки видят общую память

Это фундаментальный вопрос о Java Memory Model (JMM), который объясняет видимость данных между потоками.

Java Memory Model (JMM)

Java Memory Model определяет, как потоки взаимодействуют с памятью. Это гарантирует определённое поведение при многопоточном доступе.

Принцип 1: Happens-Before

Это основной принцип JMM. Операция A happens-before операция B, если результаты A гарантированно видны потоку, выполняющему B.

Ключевые правила happens-before:

1. Program Order (порядок в одном потоке)

int x = 1;          // Операция 1
int y = x + 1;      // Операция 2 happens-after операции 1

Операции в одном потоке выполняются по порядку программы.

2. Volatile Writes and Reads

private volatile boolean flag = false;
private int value = 0;

// Поток 1
public void writer() {
    value = 42;           // Запись
    flag = true;          // Volatile запись
}

// Поток 2
public void reader() {
    if (flag) {           // Volatile чтение
        System.out.println(value);  // Гарантированно 42!
    }
}

Volatile гарантирует:

  • Запись в volatile переменную happens-before чтение из неё
  • Предыдущие запросы happen-before volatile запись
  • Volatile чтение happens-before следующие операции

3. Monitor Lock (synchronized)

private int value = 0;

public synchronized void writer() {
    value = 42;  // Запись внутри синхронизованного блока
}

public synchronized void reader() {
    System.out.println(value);  // Гарантированно видит 42
}

Освобождение монитора happens-before получение монитора другим потоком.

4. Thread Start and Termination

private int x = 0;
private Thread t;

public void main() {
    x = 10;
    t = new Thread(() -> {
        System.out.println(x);  // Гарантированно 10!
    });
    t.start();  // start() happens-before код в потоке выполнится
}

Визуализация: без синхронизации

private boolean ready = false;
private int value = 0;

// Поток 1
public void writer() {
    value = 100;
    ready = true;  // Обычная переменная - может быть переупорядочена!
}

// Поток 2
public void reader() {
    if (ready) {
        System.out.println(value);  // Может вывести 0!
        // Компилятор и процессор переупорядочили операции
    }
}

Процессор видит эти операции независимыми и переупорядочил их в памяти!

Решение: volatile

private volatile boolean ready = false;
private int value = 0;

// Поток 1
public void writer() {
    value = 100;
    ready = true;  // Volatile гарантирует порядок!
}

// Поток 2
public void reader() {
    if (ready) {
        System.out.println(value);  // Всегда 100!
        // Volatile чтение happens-after все предыдущие операции в Потоке 1
    }
}

Принцип 2: Видимость в многопоточной программе

Без синхронизации:

class Counter {
    private int count = 0;  // Обычная переменная
    
    public void increment() {
        count++;  // Может не быть видно другим потокам!
    }
    
    public int getCount() {
        return count;  // Может быть кеширована в регистре процессора
    }
}

Процессор кэширует значение count в локальном регистре. Другой поток может видеть старое значение.

С volatile:

class Counter {
    private volatile int count = 0;
    
    public void increment() {
        count++;  // Каждая запись видна всем потокам
    }
    
    public int getCount() {
        return count;  // Всегда читает актуальное значение из памяти
    }
}

Практический пример: Double-Checked Locking

class Singleton {
    private static volatile Singleton instance;  // ❌ Без volatile опасно!
    
    public static Singleton getInstance() {
        if (instance == null) {  // Первая проверка
            synchronized (Singleton.class) {
                if (instance == null) {  // Вторая проверка
                    instance = new Singleton();  // Только одно создание
                }
            }
        }
        return instance;
    }
}

Без volatile:

  1. Поток А создаёт instance в конструкторе
  2. Запись в instance может быть переупорядочена
  3. Поток B видит instance != null, но объект не инициализирован!

volatile гарантирует правильный порядок.

Три уровня синхронизации

1. Обычные переменные (может быть видимость отложена)

private int x = 0;  // Не гарантирует видимость

2. Volatile переменные (видимость гарантирована)

private volatile int x = 0;  // Видимость + порядок

3. Synchronized (монитор блокирует доступ)

private int x = 0;

public synchronized void increment() {
    x++;  // Монопольный доступ
}

Микс: Concurrent коллекции

// ConcurrentHashMap использует volatile переменные и блокировки
Map<String, String> map = new ConcurrentHashMap<>();
map.put("key", "value");  // Thread-safe видимость

// AtomicInteger использует volatile
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();  // Thread-safe видимость

Правило visibility guarantee в JMM

Для обычной переменной (не volatile):

  • Нет гарантий видимости между потоками
  • Компилятор и процессор могут переупорядочить
  • Значение может кэшироваться в процессоре

Для volatile переменной:

  • Каждая запись видна читателям (flush cache)
  • Каждое чтение получает актуальное значение (reload cache)
  • Порядок операций не переупорядочивается

Заключение

Потоки считывают переменные по принципу happens-before JMM:

  1. Без синхронизации - нет гарантий видимости
  2. С volatile - видимость гарантирована через flush/reload операции
  3. С synchronized - монитор обеспечивает видимость и исключительный доступ
  4. Memory barrier - специальные инструкции CPU гарантируют порядок

Всегда используй volatile или synchronized для общих данных между потоками!

По какому принципу потоки считывают переменную в общей памяти | PrepBro