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

Какие проблемы будут при изменении переменной с двух потоков

2.0 Middle🔥 242 комментариев
#JVM и память#Многопоточность и асинхронность

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

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

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

Проблемы параллельного доступа к памяти

Проблема изменения общей переменной из нескольких потоков (race condition) является одной из фундаментальных сложностей в многопоточном программировании. Основные проблемы можно разделить на несколько категорий.

1. Состояние гонки (Race Condition)

Самая очевидная проблема - неопределенность результата. Поскольку порядок выполнения операций в потоках непредсказуем, итоговое значение переменной зависит от того, какой поток когда успел выполнить свою операцию.

var counter = 0

fun incrementCounter() {
    counter++ // Это НЕ атомарная операция!
}

// В двух потоках:
thread1.incrementCounter() // Читает 0, увеличивает до 1
thread2.incrementCounter() // Может прочитать 0 или 1 в зависимости от времени

// Результат может быть 1 или 2, но не детерминирован

2. Конфликты чтения-записи и записи-записи

Проблемы возникают даже при простых операциях:

Проблема записи-записи: Два потока пытаются записать разные значения в одну переменную. Результат зависит от порядка завершения операций.

Проблема чтения-записи: Один поток читает переменную, пока другой ее изменяет. Читающий поток может получить устаревшее или частично измененное значение.

3. Нарушение атомарности операций

Многие кажущиеся простыми операции на самом деле состоят из нескольких шагов:

// counter++ на уровне процессора выполняет три операции:
1. Чтение текущего значения из памяти
2. Увеличение значения на 1
3. Запись нового значения в память

Между этими шагами может вмешаться другой поток, что приводит к потере обновлений.

4. Проблема видимости изменений

Изменения, сделанные в одном потоке, могут быть не видны другим потокам из-за особенностей архитектуры процессора:

  • Кеши процессора - каждый поток может работать со своей копией данных
  • Переупорядочивание инструкций - процессор может оптимизировать порядок выполнения команд
  • Отложенная запись в основную память

Пример проблемы видимости:

// Поток 1
sharedFlag = true

// Поток 2
while(!sharedFlag) {
    // Может бесконечно ждать, если не использовать volatile
}

5. Поломка инвариантов объектов

Когда переменная - это сложный объект, одновременная модификация его полей может привести к нарушению внутренней согласованности:

class Account {
    private int balance;
    private String owner;
    
    void transfer(int amount, String newOwner) {
        this.balance -= amount; // Шаг 1
        this.owner = newOwner;  // Шаг 2
        // Между шагами объект находится в несогласованном состоянии!
    }
}

Если другой поток обратится к объекту между шагами 1 и 2, он увидит частично измененный объект.

6. Deadlock, Livelock и Starvation

Хотя это не проблемы напрямую связанные с изменением переменных, они часто возникают при попытке синхронизировать доступ:

  • Deadlock - потоки блокируют друг друга навсегда
  • Livelock - потоки постоянно меняют состояние, но не продвигаются в работе
  • Starvation - некоторый поток никогда не получает доступа к ресурсу

7. Производительность и масштабируемость

Использование блокировок для синхронизации может привести к:

  • Конкуренции за блокировки - потоки тратят время на ожидание
  • Снижению параллелизма - сериализации операций
  • Проблемы с масштабированием - при увеличении числа потоков

Решения и подходы

Для безопасного изменения переменных используют:

  1. Синхронизированные блоки и методы

    synchronized(this) {
        counter++;
    }
    
  2. Атомарные типы (AtomicInteger, AtomicReference)

    AtomicInteger atomicCounter = new AtomicInteger(0);
    atomicCounter.incrementAndGet(); // Атомарная операция
    
  3. Локальные переменные потока (ThreadLocal)

  4. Неизменяемые объекты (immutable objects)

  5. Примитивы синхронизации (ReentrantLock, Semaphore)

  6. Потокобезопасные коллекции (ConcurrentHashMap, CopyOnWriteArrayList)

Вывод

Изменение переменной из нескольких потоков без синхронизации почти всегда приводит к неопределенному поведению. Особенно критично это в Android разработке, где UI-поток постоянно взаимодействует с фоновыми потоками, работающими с данными. Правильное использование механизмов синхронизации - обязательное условие создания стабильных многопоточных приложений.