Avatar ·

Почему в Kotlin нет ключевых слов для конкурентности (synchronized, volatile)?

Всем привет! Я начинающий разработчик на Kotlin, перехожу с Java. В Java я привык использовать ключевые слова `synchronized`, `volatile`, а также блоки `synchronized` для управления доступом к общим ресурсам в многопоточных приложениях. Но когда я начал писать на Kotlin, я заметил, что в языке нет этих ключевых слов! Например, я попытался написать простой потокобезопасный счетчик: ```kotlin class Counter { private var count = 0 fun increment() { // Как мне синхронизировать этот метод? count++ } } ``` В Java я бы написал `public synchronized void increment()`, но в Kotlin такой возможности нет. Я нашел в документации, что есть `@Synchronized` аннотация и `@Volatile`, но это не то же самое, что ключевые слова. Мне кажется, что это очень странное решение для языка, который претендует на то, чтобы быть современной заменой Java. Почему создатели Kotlin отказались от этих ключевых слов? Как мне теперь правильно писать многопоточный код? Нужно ли мне всегда использовать корутины? Или есть какие-то альтернативные подходы? Буду благодарен за подробное объяснение с примерами! <<>>

Краткий ответ: Kotlin не поощряет использование низкоуровневых примитивов синхронизации

Создатели Kotlin (JetBrains) сознательно отказались от встроенных ключевых слов для конкурентности, таких как synchronized или volatile, по трем причинам:

  • Философия языка: Kotlin продвигает безопасные и декларативные подходы. Низкоуровневые блокировки (как synchronized) считаются антипаттерном, так как они легко приводят к deadlock'ам, race condition и сложному для понимания коду.
  • Корутины как основное средство: Kotlin создавался с корутинами в ядре (начиная с Kotlin 1.3). Корутины — это легковесные потоки, которые позволяют писать асинхронный код в последовательном стиле без блокировок.
  • Аннотации как компромисс: Для совместимости с Java оставлены аннотации @Synchronized и @Volatile, но они используются только при взаимодействии с Java-кодом. В чистом Kotlin их применение считается плохой практикой.

Как правильно решать вашу задачу?

Для вашего примера со счетчиком есть два правильных пути:

1. Использовать атомарные типы (для простых операций)

Для инкремента счетчика не нужна блокировка — достаточно атомарного типа из пакета java.util.concurrent.atomic:

import java.util.concurrent.atomic.AtomicInteger

class Counter {
    private val count = AtomicInteger(0)

    fun increment() {
        count.incrementAndGet()
    }

    fun getCount(): Int = count.get()
}

2. Использовать корутины (для сложной логики)

Если ваш код работает с асинхронными операциями (сеть, файлы), используйте корутины и Mutex:

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

class Counter {
    private var count = 0
    private val mutex = Mutex()

    suspend fun increment() {
        mutex.withLock {
            count++
        }
    }
}

Mutex — это аналог synchronized, но для корутин. Он не блокирует поток, а приостанавливает корутину, что гораздо эффективнее.

Когда всё же нужна синхронизация потоков?

Если вы работаете с классическими потоками (Java Thread) и не можете перейти на корутины, используйте:

  • @Synchronized — только для методов, которые будут вызываться из Java-кода.
  • synchronized(lock) { ... } — стандартная Java-конструкция, доступна в Kotlin как функция высшего порядка.
class OldStyleCounter {
    private val lock = Any()
    private var count = 0

    fun increment() {
        synchronized(lock) {
            count++
        }
    }
}

Но помните: это наследие Java, в новом коде старайтесь избегать такого подхода.

<<>>

Альтернативный взгляд: акторы и каналы

Если вы хотите полностью отказаться от блокировок, рассмотрите модель акторов. Kotlin предоставляет kotlinx.coroutines.channels и kotlinx.coroutines.flow для этого.

Пример с актором для счетчика:

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.actor

fun CoroutineScope.counterActor() = actor<Unit> {
    var count = 0
    for (msg in channel) {
        count++
    }
}

Актор гарантирует, что только одна корутина работает с состоянием в любой момент времени, без явных блокировок. Это более высокоуровневый и безопасный подход, который соответствует философии Kotlin.

Также стоит упомянуть StateFlow и SharedFlow для потокобезопасного хранения состояния в UI-приложениях. Например, в Android-разработке вы почти никогда не используете synchronized — вместо этого вы работаете с MutableStateFlow в ViewModel.