«Что значит synchronized?» — вопрос, который мучает каждого второго джуна
Если вы хоть раз писали многопоточное приложение на Java, то наверняка сталкивались с ситуацией: два потока одновременно меняют одну переменную, и в результате — баги, которые воспроизводятся раз в сто запусков. Знакомо? Именно для этого и существует ключевое слово synchronized. Но что оно на самом деле делает? Как работает «под капотом»? И главное — когда его использовать, а когда лучше обойтись без него?
В этой статье мы не просто перескажем документацию. Мы разберём популярный вопрос со Stack Overflow (тот самый, который собрал тысячи просмотров), адаптируем его под образовательный формат и покажем сравнение подходов: синхронизация на уровне метода, на уровне блока, и альтернативы из java.util.concurrent. Поехали!
Проблема: гонка потоков (Race Condition)
Представьте: у вас есть счётчик, который инкрементируют два потока. Код простой, но результат — неожиданный.
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
Запускаем два потока, каждый делает по 1000 инкрементов. Казалось бы, должно быть 2000. Но на деле — 1987, 1993, или даже 1765. Почему? Потому что операция count++ не атомарна. Она состоит из трёх шагов: чтение → увеличение → запись. Потоки «пересекаются», и одно изменение теряется.
Это и есть race condition — состояние гонки. И synchronized — один из главных инструментов борьбы с ним.
Что делает synchronized? Заглянем под капот
Ключевое слово synchronized в Java гарантирует взаимное исключение (mutex) и видимость изменений между потоками. Когда поток входит в synchronized блок, он захватывает монитор объекта (или класса). Другие потоки, пытающиеся войти в любой synchronized блок на том же мониторе, блокируются до его освобождения.
Важно: synchronized работает только с одним монитором. Если у вас два разных объекта — синхронизация не сработает.
Два уровня синхронизации
- Синхронизированный метод — блокирует весь метод, использует монитор объекта (
this). - Синхронизированный блок — блокирует только часть кода, можно указать любой объект в качестве монитора.
Давайте сравним их на практике.
Сравнение подходов: метод vs блок
1. Синхронизированный метод
Самый простой способ — добавить synchronized к сигнатуре метода.
public class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Плюсы:
- Минимум кода, легко читать.
- Гарантирует, что весь метод — атомарная операция.
Минусы:
- «Тяжёлая» блокировка — даже если внутри метода только одна строка, поток блокируется на всё время выполнения.
- Снижает производительность, если метод долгий или вызывается часто.
- Не гибкий — нельзя синхронизировать только часть логики.
2. Синхронизированный блок
Более тонкий контроль. Вы сами решаете, какой объект использовать как монитор.
public class SafeCounterWithBlock {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
Плюсы:
- Гибкость — можно синхронизировать только критическую секцию.
- Меньше шансов на deadlock, если использовать отдельный объект блокировки.
- Можно синхронизироваться на разных объектах для разных операций.
Минусы:
- Чуть больше кода.
- Нужно следить, чтобы все потоки использовали один и тот же монитор.
А что насчёт статических методов?
Если метод статический, synchronized работает на уровне класса, а не объекта. Это значит, что блокируется весь класс, а не конкретный экземпляр.
public class StaticCounter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
}
Такая синхронизация нужна, когда вы работаете с общими статическими данными. Но будьте осторожны: это может стать узким местом производительности.
Ловушки и подводные камни
Даже опытные разработчики иногда ошибаются. Вот три классические проблемы:
1. Синхронизация на строке
Никогда не используйте строки как монитор, если они не интернированы явно. Две строки с одинаковым содержанием могут быть разными объектами.
// Плохо: строки могут быть разными
synchronized ("lock") { ... }
// Хорошо: явный объект
private final Object lock = new Object();
2. Double-checked locking
Старая идиома для ленивой инициализации, которая сломана без volatile. Лучше используйте Bill Pugh Singleton или enum.
3. Deadlock
Когда поток A захватил монитор 1 и ждёт монитор 2, а поток B захватил монитор 2 и ждёт монитор 1. Классика. Решение — всегда захватывать мониторы в одном порядке.
Современные альтернативы: Lock и synchronized
В Java 5 появился пакет java.util.concurrent.locks. Класс ReentrantLock — более гибкая замена synchronized. Давайте сравним.
| Критерий | synchronized | ReentrantLock |
|---|---|---|
| Простота | Высокая, встроенный синтаксис | Требует явного lock/unlock |
| Гибкость | Низкая | Высокая: tryLock, timed lock, fairness policy |
| Производительность | Хорошая для простых случаев | Лучше при высокой конкуренции |
| Deadlock prevention | Только порядок захвата | tryLock() позволяет избе
БлогиРекомендаций по книгамЧитайте также |