Для чего используется ключевое слово synchronized в Java?

Ответ

Ключевое слово synchronized в Java используется для обеспечения взаимного исключения (mutual exclusion) и синхронизации потоков при доступе к общим ресурсам, предотвращая состояние гонки (race condition).

Оно может применяться двумя способами:

  1. Синхронизированный блок кода:

    public void increment() {
        synchronized (this) { // Монитором служит объект 'this'
            count++; // Критическая секция
        }
    }
    • В скобках указывается объект-монитор, блокировка которого будет захвачена перед выполнением кода в блоке.
  2. Синхронизированный метод:

    public synchronized void decrement() {
        count--; // Весь метод является критической секцией
    }
    • Для нестатического метода монитором служит объект this (экземпляр класса).
    • Для статического метода монитором служит объект класса (Class object).

Как это работает:

  • Чтобы войти в синхронизированный блок или метод, поток должен получить блокировку (lock) на указанном объекте-мониторе.
  • Если блокировка уже удерживается другим потоком, текущий поток переходит в состояние BLOCKED и ждет ее освобождения.
  • Когда поток выходит из синхронизированной области (даже из-за исключения), блокировка автоматически освобождается.

Важные замечания:

  • synchronized гарантирует видимость изменений (реализует happens-before), поэтому изменения, сделанные в синхронизированном блоке, становятся видимыми всем последующим потокам, которые захватят ту же блокировку.
  • Это реентерабельная (reentrant) блокировка: поток, уже владеющий блокировкой, может снова захватить ее (например, при рекурсивном вызове синхронизированного метода).
  • Недостатки: Может приводить к взаимным блокировкам (deadlock) и снижению производительности из-за сериализации выполнения потоков. Для более сложных сценариев часто предпочтительнее использовать примитивы из пакета java.util.concurrent.locks.

Ответ 18+ 🔞

А, синхронизация в Java! Ну, это как раз та тема, где можно так накосячить, что потом будешь неделю искать, почему у тебя данные, как говно в проруби, то всплывают, то тонут.

Вот смотри, есть у тебя общая переменная, count там какой-нибудь. И бегут к ней десять потоков, как голодные псы на миску. Все хотят её увеличить. Без контроля — это будет пиздец, прости мой французский. Один поток прочитал, что count = 5, второй тоже успел прочитать 5, пока первый ещё не записал. Оба прибавили единичку и записали обратно 6. А должно было стать 7! Вот это и есть состояние гонки, или race condition, когда результат зависит от того, кто какого хуя первым добежал.

Чтобы такого не было, придумали ключевое слово synchronized. Это такой здоровенный мужик с дубиной у входа в критическую секцию. Хочешь зайти — получи пропуск (блокировку). Нет пропуска — стой, блядь, и жди, пока предыдущий не выйдет.

Применять его можно двумя основными способами, как дубинкой по лбу:

  1. Синхронизированный блок. Ты сам указываешь, на каком объекте (мониторе) будешь крутить замок.

    public void increment() {
        synchronized (this) { // Замок крутится на объекте 'this'. Можно на любом другом, хоть на отдельном пустом Object lock = new Object();
            count++; // Вот эта святая святых — критическая секция
        }
    }

    Удобно, когда синхронизировать нужно не весь метод, а только его кусок. Меньше тормозить будет.

  2. Синхронизированный метод. Проще, но грубее.

    public synchronized void decrement() {
        count--; // Весь метод теперь под замком
    }

    Тут есть нюанс, ёпта! Если метод нестатический, то замок крутится на объекте this. А если статический — то на самом классе (SomeClass.class). Перепутать — и ты уже синхронизируешься на разных вещах, а состояние гонки остаётся, как заноза в жопе.

Как это работает, если по-простому: Поток подходит, видит — замок свободен. Хватает его и заходит работать. Остальные потоки упираются лбом в закрытую дверь и переходят в состояние BLOCKED (заблокирован), тихо материясь в очереди. Как первый выйдет (даже если его выкинет исключением), он замок бросит. И тогда его подхватит самый шустрый из ожидающих.

Что ещё важно, чтобы не обосраться:

  • Видимость изменений. synchronized — это не только про очередь. Он ещё гарантирует, что если один поток в синхронизированном блоке поменял переменную, то следующий поток, зашедший под тот же замок, увидит это изменение. Без этого кэши процессоров могли бы показывать каждому потоку свою старую хуйню.
  • Реентерабельность. Если поток уже владеет замком, он может зайти ещё в один синхронизированный блок, который требует тот же замок. Это чтобы ты сам себя не заблокировал, например, вызвав один synchronized-метод из другого synchronized-метода того же объекта.
  • Недостатки. А они, блядь, есть! Во-первых, взаимная блокировка (deadlock). Два потока ждут друг от друга замки и стоят вечно, как два упрямых осла. Во-вторых, производительность. Если синхронизировать всё подряд, то потоки будут больше стоять в очереди, чем работать. Для сложных сценариев уже давно придумали более хитрые штуки из java.util.concurrent.locks — там и таймауты есть, и раздельные lock/unlock. Но synchronized — это классика, основа, которую надо понимать, как таблицу умножения.