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

Для чего нужен synchronized?

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

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

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

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

Для чего нужен synchronized

synchronized — это ключевое слово в Kotlin/Java для синхронизации доступа к ресурсам в многопоточной среде. Оно обеспечивает, чтобы только один поток одновременно мог выполнять защищённый код, предотвращая состояние гонки (race condition) и гарантируя thread-safety.

Проблема без synchronized

Представим счётчик, используемый несколькими потоками:

class Counter {
    var count = 0
    
    fun increment() {
        count++  // опасно без синхронизации!
    }
}

val counter = Counter()

repeat(5) {
    thread {
        repeat(1000) {
            counter.increment()
        }
    }
}

Thread.sleep(2000)
println(counter.count)  // Результат: не 5000, например 3847!

Почему результат неправильный?

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

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

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

Поток 1: читает count=5
Поток 2: читает count=5 (!)
Поток 1: пишет count=6
Поток 2: пишет count=6 (вместо 7!)

Инкремент потока 2 потерян, произойдёт потеря данных.

synchronized для методов

Найпростой способ — синхронизировать весь метод:

class Counter {
    var count = 0
    
    synchronized fun increment() {
        count++  // теперь безопасно
    }
}

Теперь только один поток может одновременно вызвать increment(). Остальные будут ждать освобождения блокировки.

synchronized блок

Можно синхронизировать только часть кода:

class BankAccount {
    private var balance = 1000.0
    private val lock = Any()
    
    fun withdraw(amount: Double) {
        // Это может быть не синхронизировано
        val fee = calculateFee(amount)
        
        // Только критическая секция синхронизирована
        synchronized(lock) {
            if (balance >= amount) {
                balance -= amount
                recordTransaction(amount)
            }
        }
    }
}

Это более эффективно, так как только существенная операция защищена.

Синхронизация на объекте

val monitor = Any()  // объект для синхронизации

synchronized(monitor) {
    // Код выполняется с блокировкой на monitor
    // Другие потоки будут ждать
}

synchronized(monitor) {
    // Повторное входит в тот же monitor
    // (реентрантная блокировка)
}

Гарантии synchronized

1. Взаимное исключение

Только один поток может быть в synchronized блоке одновременно:

val data = mutableListOf<Int>()
val lock = Any()

thread {
    synchronized(lock) {
        data.add(1)
        Thread.sleep(1000)  // другой поток подождёт
    }
}

thread {
    synchronized(lock) {
        data.add(2)  // это выполнится ПОСЛЕ первого потока
    }
}

2. Видимость памяти (Memory Visibility)

synchronized гарантирует, что изменения, сделанные одним потоком, видны другим:

class Flag {
    var done = false
    
    fun markDone() {
        synchronized(this) {
            done = true  // видимо для других потоков
        }
    }
    
    fun isDone(): Boolean = synchronized(this) {
        done  // видно последнее значение
    }
}

Без synchronized второй поток может читать старое значение из кеша.

Практический пример: потокобезопасный Singleton

class Singleton private constructor() {
    companion object {
        private var instance: Singleton? = null
        
        fun getInstance(): Singleton {
            if (instance == null) {
                synchronized(this) {
                    if (instance == null) {  // Double-Checked Locking
                        instance = Singleton()
                    }
                }
            }
            return instance!!
        }
    }
}

Недостатки synchronized

  1. Производительность: Блокировка замедляет выполнение
  2. Deadlock: Неправильное использование может привести к зависаниям
  3. Контроль: Нет тайм-аутов или попыток неблокирующего захвата

Альтернативы

1. AtomicInteger (для простых операций)

val counter = AtomicInteger(0)
repeat(5) {
    thread {
        repeat(1000) {
            counter.incrementAndGet()  // атомарная операция
        }
    }
}

Thread.sleep(2000)
println(counter.get())  // 5000 - правильно!

2. ReentrantLock (больше контроля)

val lock = ReentrantLock()
var count = 0

lock.lock()
try {
    count++
} finally {
    lock.unlock()
}

// С тайм-аутом
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        count++
    } finally {
        lock.unlock()
    }
}

3. Coroutines с Mutex (асинхронный вариант)

val mutex = Mutex()
var count = 0

suspend fun incrementAsync() {
    mutex.withLock {
        count++  // не блокирует поток
    }
}

Правило выбора

СитуацияРешение
Простой счётчикAtomicInteger
Защита нескольких переменныхsynchronized блок
Нужен тайм-аутReentrantLock
Асинхронный код (suspend)Mutex из coroutines
Несложная логикаVolatile

Итого

synchronized — это фундаментальный инструмент для потокобезопасности:

  • Предотвращает race conditions
  • Гарантирует видимость памяти
  • Легко использовать для начинающих

Но в современном коде Kotlin предпочитают coroutines и Mutex, так как они более гибкие и лучше масштабируются.

Для чего нужен synchronized? | PrepBro