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

Что такое состояние гонки (Race condition)?

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

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

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

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

Race Condition — конкурентная гонка между потоками

Race condition (состояние гонки) — это ошибка в многопоточной программе, когда результат зависит от порядка выполнения операций несколькими потоками. Потоки конкурируют за доступ к общему ресурсу, и в зависимости от того, какой поток «выиграет гонку», результат будет разным.

Классический пример — увеличение счётчика

var counter = 0 // Общая переменная для двух потоков

fun thread1() {
    for (i in 1..1000) {
        counter++ // Операция не атомарна!
    }
}

fun thread2() {
    for (i in 1..1000) {
        counter++ // Операция не атомарна!
    }
}

// Запуск обоих потоков
thread(block = ::thread1).start()
thread(block = ::thread2).start()

// Ожидаемый результат: 2000
// Фактический результат: может быть любое число от 1000 до 2000!
// Часто получаем: 1234, 1567, 1892 и т.д.

Почему counter++ ненадежна?

Операция counter++ состоит из трёх шагов:

  1. READ: прочитать текущее значение (например, 5)
  2. MODIFY: увеличить на 1 (5 + 1 = 6)
  3. WRITE: записать обратно (записать 6)

Если два потока выполняют это одновременно:

Поток 1:           Поток 2:           counter
  READ (5)   →                           5
                    READ (5)              5
  MODIFY (6)  →                          5
                    MODIFY (6)            5
  WRITE (6)   →                          6
                    WRITE (6)             6

// Результат: 6 вместо 7 (потеря обновления!)

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

Bad — Race condition в UI:

var userBalance = 1000 // Доступно из основного потока и фонового

fun withdrawMoney(amount: Int) {
    Thread {
        if (userBalance >= amount) { // RACE: баланс может измениться
            userBalance -= amount // RACE: может быть negative balance
            updateUI("Withdrawn: $amount")
        }
    }.start()
}

fun deposit(amount: Int) {
    userBalance += amount // RACE: конкурирует с withdrawal
}

// Сценарий:
// Баланс: 1000
// 1. Поток withdraw читает баланс: 1000
// 2. Поток deposit меняет баланс: 1000 + 500 = 1500
// 3. Поток withdraw вычитает: 500 (хотя баланс уже обновлен)
// Результат: неправильный баланс!

Решение 1: synchronized

var userBalance = 1000
val lock = Object()

fun withdrawMoney(amount: Int) {
    Thread {
        synchronized(lock) { // Гарантирует эксклюзивный доступ
            if (userBalance >= amount) {
                userBalance -= amount
                updateUI("Withdrawn: $amount")
            }
        }
    }.start()
}

fun deposit(amount: Int) {
    synchronized(lock) {
        userBalance += amount
    }
}

Решение 2: Atomic классы

import java.util.concurrent.atomic.AtomicInteger

val counter = AtomicInteger(0)

fun thread1() {
    for (i in 1..1000) {
        counter.incrementAndGet() // Атомарная операция
    }
}

fun thread2() {
    for (i in 1..1000) {
        counter.incrementAndGet() // Атомарная операция
    }
}

// Результат: всегда 2000 ✅

Решение 3: Volatile + правильная синхронизация

@Volatile
var userBalance = 1000

fun withdrawMoney(amount: Int) {
    synchronized(this) {
        if (userBalance >= amount) {
            userBalance -= amount
        }
    }
}

Решение 4: Coroutines + Mutex (Kotlin)

var userBalance = 1000
val balanceLock = Mutex()

suspend fun withdrawMoney(amount: Int) {
    balanceLock.withLock { // Гарантирует синхронизацию
        if (userBalance >= amount) {
            userBalance -= amount
        }
    }
}

Типичные места Race Conditions в Android

1. SharedPreferences (не thread-safe для write):

// ❌ Может быть race condition
SharedPreferences.edit().putString("key", value).apply()

// ✅ Правильно
SharedPreferences.edit().putString("key", value).commit() // Синхронно

2. Database access:

// ❌ Race condition
val user = getUserFromDb(id)
user.name = "New Name"
saveUserToDb(user)

// ✅ Использовать транзакции
database.withTransaction {
    val user = getUserFromDb(id)
    user.name = "New Name"
    saveUserToDb(user)
}

3. Check-then-act pattern:

// ❌ Race condition между проверкой и действием
if (isLoggedIn) { // RACE: статус может измениться
    loadUserData() // RACE: может быть logout
}

// ✅ Синхронизированная проверка и действие
synchronized(lock) {
    if (isLoggedIn) {
        loadUserData()
    }
}

Инструменты для поиска Race Conditions

  • ThreadSanitizer (в Java/Kotlin виртуальных машинах)
  • Intellij IDEA — анализ потенциальных race conditions
  • JUnit tests с множественными потоками
@Test
fun testConcurrentAccess() {
    repeat(100) { // Запускаем 100 раз для поиска race conditions
        val counter = AtomicInteger(0)
        val threads = (1..10).map {
            Thread {
                repeat(100) {
                    counter.incrementAndGet()
                }
            }
        }
        threads.forEach { it.start() }
        threads.forEach { it.join() }
        assertEquals(1000, counter.get())
    }
}

Правила безопасной многопоточности

  • Избегай общего изменяемого состояния (mutable shared state)
  • Синхронизируй доступ к общим ресурсам (synchronized, Mutex, AtomicXxx)
  • Используй volatile для флагов видимости между потоками
  • Предпочитай immutable данные и coroutines вместо raw threads
  • Тестируй многопоточный код с большим количеством итераций

Вывод

Race condition — это критическая ошибка, которая возникает, когда несколько потоков конкурируют за доступ к общему ресурсу без синхронизации. Результат выполнения становится недетерминированным. Используй synchronized, volatile, Atomic классы или coroutines с Mutex для защиты критических секций кода.