«Synchronized» в Java: что это и как работает на примерах

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

«Что значит 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. Давайте сравним.

КритерийsynchronizedReentrantLock
ПростотаВысокая, встроенный синтаксисТребует явного lock/unlock
ГибкостьНизкаяВысокая: tryLock, timed lock, fairness policy
ПроизводительностьХорошая для простых случаевЛучше при высокой конкуренции
Deadlock preventionТолько порядок захватаtryLock() позволяет избе

Блоги

Рекомендаций по книгам