Гонка данных (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++
включает три шага:
counter
.counter
.Если два потока выполняют эти шаги одновременно, они могут прочитать одно и то же значение, увеличить его и записать одно и то же новое значение, что приводит к потере одного инкремента.
Использование ключевого слова 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
для синхронизации метода:
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
}
Для операций инкремента можно использовать атомарные переменные, которые обеспечивают атомарные операции без необходимости явной блокировки:
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, но результат может быть меньше из-за гонки данных
}
В этом случае можно решить проблему с помощью синхронизации или атомарных переменных, чтобы гарантировать корректное обновление количества проданных товаров.