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

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

1.2 Junior🔥 141 комментариев
#Многопоточность

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

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

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

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

В Java потоки (threads) записывают в общую память в соответствии с Memory Model Java, который определён в Java Language Specification.

Java Memory Model (JMM)

Java Memory Model — это набор правил, которые гарантируют видимость (visibility) и атомарность (atomicity) операций между потоками.

Как потоки видят память

Каждый поток имеет локальный кеш (L1, L2, L3):

ЦПУ 1             ЦПУ 2             ЦПУ 3
┌──────────┐     ┌──────────┐     ┌──────────┐
│L1 Cache  │     │L1 Cache  │     │L1 Cache  │
│ x = 10   │     │ x = 0    │     │ x = 0    │
└──────────┘     └──────────┘     └──────────┘
      ↓                ↓                ↓
┌─────────────────────────────────────────────┐
│         Main Memory (Heap)                   │
│            x = 10                           │
└─────────────────────────────────────────────┘

Проблема: Если Поток 1 меняет x, Поток 2 может не видеть изменение!

Принципы записи в общую память

1. Happens-Before отношения (главный принцип JMM)

Oперация A "happens-before" операции B означает, что:

  • Все действия A завершены ДО B
  • Все видимые эффекты A видны B
int x = 0;

// Поток 1
x = 10;                    // Action A
volatile_flag = true;      // Action B (volatile запись)

// Поток 2
if (volatile_flag) {       // Action C (volatile чтение)
    System.out.println(x); // Гарантированно 10!
}

Почему это работает:

  • Volatile запись в Action B → Happens-Before sync с памятью
  • Volatile чтение в Action C → видит B
  • Поэтому x = 10 видно в C

2. Ключевые слова для synchronization

synchronized

private int counter = 0;

public synchronized void increment() {
    counter++; // Атомарная операция в контексте метода
}

public synchronized int getCounter() {
    return counter;
}

Как работает:

  • Каждый объект имеет монитор (lock)
  • synchronized блокирует этот монитор
  • Все потоки видят обновления
public synchronized void method() {
    // Happens-Before: монитор acquire
    // ... действия ...
    // Happens-Before: монитор release
}

Проблема: synchronized может быть медленным на высоконагруженных системах.

volatile

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

// Поток 1
value = 100;
flag = true;  // Гарантирует запись в основную память

// Поток 2
if (flag) {   // Гарантирует чтение из основной памяти
    System.out.println(value); // 100!
}

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

  • Видимость — изменения видны другим потокам
  • Атомарность — не защищает от race conditions

ОПАСНО:

private volatile int counter = 0;

public void increment() {
    counter++; // НЕ АТОМАРНО! Может быть потеря обновлений
    // counter++ = read, increment, write (3 операции)
}

ПРАВИЛЬНО:

private volatile int counter = 0;

public synchronized void increment() {
    counter++; // Теперь безопасно
}

Happens-Before гарантии в Java

ОперацияHappens-Before
Запись в volatile → Чтение из volatile✅ Гарантирует видимость
synchronized выход → synchronized вход✅ Гарантирует видимость
Thread.start() → действия потока✅ Гарантирует видимость
действия потока → Thread.join()✅ Гарантирует видимость
Без синхронизации❌ NO GUARANTEE

Реальный пример

public class VisibilityExample {
    
    // БЕЗ volatile — опасно!
    private int value = 0;
    private boolean ready = false;
    
    public void writer() {
        value = 100;  // Может остаться в локальном кеше
        ready = true; // Другой поток может не видеть!
    }
    
    public void reader() {
        while (!ready) { // Может зависнуть в бесконечном цикле!
            // Поток никогда не видит ready = true
        }
        System.out.println(value); // Может быть 0 или 100
    }
}

ИСПРАВЛЕННЫЙ вариант:

public class VisibilityFixed {
    
    private volatile int value = 0;      // Видимость
    private volatile boolean ready = false; // Видимость
    
    public void writer() {
        value = 100;    // Немедленно в основную память
        ready = true;   // Все видят
    }
    
    public void reader() {
        while (!ready) {
            // Поток видит обновление ready
        }
        System.out.println(value); // Гарантированно 100
    }
}

AtomicInteger / AtomicReference (для сложных случаев)

private AtomicInteger counter = new AtomicInteger(0);

public void increment() {
    counter.incrementAndGet(); // Атомарно + видимость
}

public int getCounter() {
    return counter.get();
}

AtomicInteger использует:

  • Volatile переменные внутри
  • CAS (Compare-And-Swap) операции на уровне ЦПУ
  • Без блокировок (lock-free)

Порядок операций (Ordering)

Java гарантирует happens-before порядок только для:

✅ Synchronized блоки ✅ Volatile переменные ✅ Atomic классы ✅ Thread.start() и Thread.join() ✅ Lock'и (ReentrantLock и др.)

БЕЗ гарантий:

private int x = 0;
private int y = 0;

// Поток 1
x = 1; // Может быть переупорядочена!
y = 1;

// Поток 2
if (y == 1) {
    System.out.println(x); // Может быть 0!
}

Best Practices

  1. Используй volatile для флагов:

    private volatile boolean shutdown = false;
    
  2. Используй synchronized для критических секций:

    synchronized (lock) {
        // сложные операции
    }
    
  3. Используй AtomicInteger для счётчиков:

    private AtomicInteger count = new AtomicInteger(0);
    count.incrementAndGet();
    
  4. Используй ReentrantLock для сложной синхронизации:

    private Lock lock = new ReentrantLock();
    lock.lock();
    try {
        // критическая секция
    } finally {
        lock.unlock();
    }
    

Вывод

Потоки записывают в общую память в соответствии с Java Memory Model:

  • Volatile гарантирует видимость (но не атомарность)
  • Synchronized гарантирует видимость + атомарность (но медленнее)
  • Happens-Before отношения — основной инструмент JMM
  • Без синхронизации — нет гарантий на многопроцессорных системах