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

Каким будет значение переменной-счетчика после выполнения 100 корутин, каждая из которых 1000 раз увеличит её на единицу?

2.4 Senior🔥 131 комментариев
#JVM и память#Многопоточность и асинхронность

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

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

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

Анализ проблемы

Классический вопрос о многопоточности (concurrency) и состоянии гонки (race condition). Корректный ответ зависит от контекста реализации, но в подавляющем большинстве случаев в Kotlin с корутинами и непотокобезопасным (non-thread-safe) счетчиком значение после выполнения будет меньше 100 000, и точное значение предсказать невозможно.

Основная причина: Race Condition (Состояние гонки)

Если 100 корутин запускаются параллельно в разных потоках (например, с Dispatchers.Default) и обращаются к общей изменяемой переменной без синхронизации, операции чтения и записи могут перекрываться.

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() = runBlocking {
    var counter = 0
    val jobs = List(100) {
        launch(Dispatchers.Default) {
            repeat(1000) {
                val temp = counter // 1. Чтение текущего значения
                // Здесь может быть переключение контекста!
                counter = temp + 1 // 2. Запись увеличенного значения
            }
        }
    }
    jobs.forEach { it.join() }
    println("Counter = $counter")
}

Что происходит:

  1. Две корутины (А и Б) одновременно читают значение counter, например, 5.
  2. Корутина А увеличивает своё значение до 6 и записывает его.
  3. Корутина Б уже прочитала 5, увеличивает до 6 и тоже записывает.
  4. В итоге после двух операций инкремента счетчик равен 6, а не 7.

Почему это происходит с корутинами?

  • Корутины — легковесные, но не волшебные. При использовании многопоточного диспетчера (Dispatchers.Default, Dispatchers.IO или собственного пула) корутины выполняются на реальных потоках.
  • Suspend-функции могут приостанавливаться в точках suspend, но операция counter++ — это не атомарная последовательность "чтение-изменение-запись" в контексте многопоточности.
  • Даже если использовать однопоточный диспетчер (например, Dispatchers.Main или newSingleThreadContext), то гонки не будет, и результат будет ровно 100 000. Но в вопросе явно подразумевается параллельное выполнение.

Как получить предсказуемый результат (100 000)?

Для этого нужно использовать потокобезопасные (thread-safe) конструкции.

Вариант 1: Atomic

import java.util.concurrent.atomic.AtomicInteger

val atomicCounter = AtomicInteger(0)
fun main() = runBlocking {
    val jobs = List(100) {
        launch(Dispatchers.Default) {
            repeat(1000) {
                atomicCounter.incrementAndGet() // Атомарная операция
            }
        }
    }
    jobs.forEach { it.join() }
    println("Counter = ${atomicCounter.get()}") // Гарантировано 100000
}

Вариант 2: Синхронизация (Mutex)

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val mutex = Mutex()
var counter = 0
fun main() = runBlocking {
    val jobs = List(100) {
        launch(Dispatchers.Default) {
            repeat(1000) {
                mutex.withLock { // Только одна корутина работает с переменной
                    counter++
                }
            }
        }
    }
    jobs.forEach { it.join() }
    println("Counter = $counter") // Гарантировано 100000
}

Вариант 3: Ограничение диспетчером (если позволяет логика)

val singleThreadContext = newSingleThreadContext("MyThread")
var counter = 0
fun main() = runBlocking {
    val jobs = List(100) {
        launch(singleThreadContext) { // Все корутины в одном потоке
            repeat(1000) {
                counter++ // Гонки нет, так как нет параллелизма
            }
        }
    }
    jobs.forEach { it.join() }
    println("Counter = $counter") // Гарантировано 100000
}

Итог

  • Без синхронизации: Результат будет случайным числом меньше 100 000 (например, 95673, 98211 и т.д.). Чем больше корутин/потоков, тем сильнее расхождение.
  • С синхронизацией или атомарными типами: Результат будет ровно 100 000.

Ключевой вывод для собеседования: Простое использование корутин не решает проблему синхронизации доступа к изменяемому состоянию (mutable state). Корутины упрощают асинхронный код, но для потокобезопасности по-прежнему требуются традиционные механизмы: атомарные переменные, мьютексы или проектирование без разделяемого изменяемого состояния (immutable state, акторы).

Каким будет значение переменной-счетчика после выполнения 100 корутин, каждая из которых 1000 раз увеличит её на единицу? | PrepBro