Synchronized: примеры (JAVA)
synchronizedОсновные сведения
Ключевое слово synchronized в Java используется для обеспечения взаимного исключения при работе с разделяемыми ресурсами в многопоточной среде. Оно позволяет только одному потоку одновременно выполнять критическую секцию кода, тем самым гарантируя атомарность и видимость изменений.
Область применения: синхронизация методов (экземпляров и статических) и синхронизация блоков.
Аргументы: Для синхронизированного метода аргументов нет – монитор выбирается автоматически (для метода экземпляра это объект this, для статического – объект класса Class). Для синхронизированного блока указывается объект-монитор, например synchronized (lockObject) { ... }.
Возвращаемое значение: ключевое слово synchronized не возвращает значения; оно лишь управляет блокировкой. Метод, помеченный как synchronized, возвращает то, что возвращает его логика.
Синхронизация в Java является реентерабельной: если один поток уже удерживает монитор, он может повторно войти в другие синхронизированные блоки на том же объекте без блокировки самого себя.
Примеры использования
Пример 1: синхронизированный метод экземпляра
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}(Результат: при одновременном вызове increment() из нескольких потоков значение count всегда корректно.)
Пример 2: синхронизированный статический метод
public class StaticCounter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
}Блокировка происходит на уровне класса, а не экземпляра.
Пример 3: синхронизированный блок с явным объектом
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// критическая секция
}
}Позволяет синхронизировать только часть метода, уменьшая конкуренцию.
Альтернативы в Java
Помимо synchronized, в Java доступны другие механизмы синхронизации:
- ReentrantLock – более гибкая блокировка с возможностью прерывания, попытки захвата без ожидания (tryLock) и поддержкой нескольких условий (Condition). Предпочтителен, когда требуется тайм-аут или несколько очередей ожидания.
- ReadWriteLock – разделяет блокировки на чтение и запись, позволяя множеству потоков одновременно читать данные при отсутствии записи. Эффективен для сценариев с частым чтением и редкой записью.
- Semaphore – ограничивает количество потоков, одновременно работающих с ресурсом, не обязательно взаимное исключение.
- CountDownLatch, CyclicBarrier – для координации потоков без явной блокировки.
Выбор между synchronized и Lock зависит от требований: для простых сценариев synchronized лаконичнее и менее подвержен ошибкам; для сложного управления блокировками (тайм-ауты, условия) лучше использовать Lock.
Аналоги в других языках
PHP: нет встроенного synchronized. Используют Mutex из расширения pthreads (для многопоточности), либо файловые блокировки.
$mutex = new Mutex();
$mutex->lock();
// критическая секция
$mutex->unlock();JavaScript: однопоточный, но в асинхронном коде может потребоваться блокировка через async/await и очереди. Используют библиотеки (например, async-lock).
const lock = new AsyncLock();
await lock.acquire('key', async () => {
// критическая секция
});Python: модуль threading содержит Lock, RLock, Semaphore.
import threading
lock = threading.Lock()
with lock:
# критическая секцияSQL: транзакции и блокировки строк (SELECT ... FOR UPDATE).
C#: оператор lock работает аналогично synchronized.
private readonly object lockObject = new object();
lock (lockObject) { /* критическая секция */ }Lua: стандартных средств нет, используют корутины или библиотеки (lua-lock).
Go: встроенные mutex (sync.Mutex) и каналы для синхронизации.
var mu sync.Mutex
mu.Lock()
// критическая секция
mu.Unlock()Kotlin: синтаксис @Synchronized аннотация или synchronized() аналогичен Java, также доступны корутины с Mutex.
@Synchronized
fun doSomething() { ... }Типичные ошибки
1. Синхронизация на изменяемом объекте. Если объект-монитор (например, поле) может измениться, разные потоки будут использовать разные мониторы.
private Object lock = new Object();
public void badSync() {
synchronized (lock) {
lock = new Object(); // опасно!
}
}Исправить: сделать поле final.
2. Deadlock из-за вложенных synchronized. Порядок захвата мониторов должен быть согласован.
// Thread 1 synchronized (a) { synchronized (b) {} }
// Thread 2 synchronized (b) { synchronized (a) {} }Решение: всегда захватывать мониторы в одном порядке.
3. Синхронизация на String-литералах. Одинаковые литералы интернируются и могут случайно пересекаться.
synchronized ("lock") { ... }Лучше использовать new Object() или специальный замок.
4. Чрезмерная синхронизация. Синхронизация всего метода, когда достаточно части кода, снижает производительность.
5. Отсутствие volatile для флагов. Синхронизированный блок гарантирует видимость, но если флаг изменяется вне блока, требуется volatile.
Изменения в последних версиях
Ключевое слово synchronized оставалось стабильным долгое время. Основные изменения касаются внутренней реализации:
- Java 6: оптимизация блокировок (biased locking, lightweight locking, lock coarsening, lock elimination).
- Java 15: отключение biased locking по умолчанию (JEP 374) из-за сложностей с поддержкой в современных средах.
- Java 18: окончательное удаление biased locking (JEP 421).
Эти изменения не влияют на синтаксис, но могут изменить производительность. Для большинства приложений synchronized остаётся эффективным.
Расширенные примеры
Пример 1: Двойная проверка блокировки (Double-checked locking).
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}Требует volatile для гарантии видимости и предотвращения переупорядочивания.
Пример 2: Использование synchronized в лямбда-выражениях.
final Object lock = new Object();
list.parallelStream().forEach(item -> {
synchronized (lock) {
// потокобезопасная операция
}
});Однако лучше использовать ConcurrentHashMap или Collections.synchronizedList.
Пример 3: Синхронизация с несколькими объектами (тонкое зерно).
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void methodA() {
synchronized (lock1) { /* работа с ресурсом A */ }
}
public void methodB() {
synchronized (lock2) { /* работа с ресурсом B */ }
}Позволяет параллельно выполнять methodA и methodB, если они не зависят друг от друга.
Пример 4: Использование synchronized с Collections.synchronizedMap.
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
// Доступ к map безопасен, но при итерации требуется ручная синхронизация:
synchronized (map) {
for (String key : map.keySet()) { ... }
}