Гонка данных (Race Condition)

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

var counter = 0

fun main() {
    val threads = List(100) {
        Thread {
            repeat(1000) {
                counter++ // Гонка данных
            }
        }
    }
    threads.forEach { it.start() }
    threads.forEach { it.join() }
    println(counter) // Ожидаем 100000, но результат непредсказуем
}

В этом примере 100 потоков одновременно увеличивают значение переменной counter. Ожидаемое значение — 100000 (100 потоков по 1000 инкрементов каждый), но из-за гонки данных результат может быть другим.

Причина гонки данных

Гонка данных возникает, когда несколько потоков пытаются одновременно прочитать и изменить один и тот же ресурс. Это происходит потому, что операции чтения, изменения и записи ресурса не атомарны. Например, операция counter++ включает три шага:

  1. Прочитать текущее значение counter.
  2. Увеличить значение на 1.
  3. Записать новое значение обратно в counter.

Если два потока выполняют эти шаги одновременно, они могут прочитать одно и то же значение, увеличить его и записать одно и то же новое значение, что приводит к потере одного инкремента.

Решение проблемы гонки данных

Синхронизированные блоки (Synchronized Blocks)

Использование ключевого слова synchronized в Kotlin для блокировки доступа к критической секции кода:

var counter = 0
val lock = Any()

fun main() {
    val threads = List(100) {
        Thread {
            repeat(1000) {
                synchronized(lock) {
                    counter++
                }
            }
        }
    }

    threads.forEach { it.start() }
    threads.forEach { it.join() }

    println(counter) // Ожидаем 100000
}

В этом примере synchronized(lock) гарантирует, что только один поток в любой момент времени может выполнять блок кода, защищенный этой блокировкой.

Синхронизированные методы (Synchronized Methods)

Можно использовать аннотацию @Synchronized для синхронизации метода:

var counter = 0

@Synchronized
fun increment() {
    counter++
}

fun main() {
    val threads = List(100) {
        Thread {
            repeat(1000) {
                increment()
            }
        }
    }

    threads.forEach { it.start() }
    threads.forEach { it.join() }

    println(counter) // Ожидаем 100000
}

Атомарные переменные (Atomic Variables)

Для операций инкремента можно использовать атомарные переменные, которые обеспечивают атомарные операции без необходимости явной блокировки:

import java.util.concurrent.atomic.AtomicInteger

val atomicCounter = AtomicInteger(0)

fun main() {
    val threads = List(100) {
        Thread {
            repeat(1000) {
                atomicCounter.incrementAndGet()
            }
        }
    }

    threads.forEach { it.start() }
    threads.forEach { it.join() }

    println(atomicCounter.get()) // Ожидаем 100000
}

Пример из реальной жизни

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

var soldItems = 0

fun sellItem() {
    soldItems++
}

fun main() {
    val threads = List(2) {
        Thread {
            repeat(1000) {
                sellItem()
            }
        }
    }

    threads.forEach { it.start() }
    threads.forEach { it.join() }

    println(soldItems) // Ожидаем 2000, но результат может быть меньше из-за гонки данных
}

В этом случае можно решить проблему с помощью синхронизации или атомарных переменных, чтобы гарантировать корректное обновление количества проданных товаров.