Как работает ключевое слово synchronized в Java?

Ответ

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

Принцип работы: Каждый объект в Java имеет связанную с ним внутреннюю блокировку (intrinsic lock или monitor lock). Когда поток входит в synchronized блок или метод, он захватывает эту блокировку. Другие потоки, пытающиеся войти в любой synchronized блок, защищенный той же самой блокировкой, будут заблокированы до ее освобождения.

Два способа использования:

  1. Синхронизированный метод: Блокировка захватывается на объекте this (для нестатических методов) или на объекте класса Class (для статических методов).

    public synchronized void increment() {
        counter++; // Потокобезопасно
    }
  2. Синхронизированный блок: Позволяет указать объект-блокировку явно, что дает большую гибкость и может уменьшить область блокировки.

    private final Object lock = new Object();
    private int counter;
    
    public void increment() {
        // Только этот блок кода синхронизирован, а не весь метод
        synchronized(lock) {
            counter++;
        }
    }

Важные аспекты (happens-before):

  • Все изменения, сделанные в synchronized блоке, гарантированно видны любому другому потоку, который впоследствии войдет в блок, синхронизированный по той же самой блокировке.
  • Это решает проблемы с кешами процессоров и переупорядочиванием инструкций.

Недостатки и альтернативы:

  • Deadlock: Риск взаимной блокировки потоков.
  • Производительность: Захват и освобождение блокировок имеют накладные расходы.
  • Альтернативы из java.util.concurrent: Для более сложных сценариев используйте ReentrantLock, Semaphore, атомарные классы (AtomicInteger) или concurrent-коллекции (ConcurrentHashMap).

Ответ 18+ 🔞

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

Вот смотри, есть у тебя объект. У каждого объекта, как у нормального пацана во дворе, есть своя внутренняя блокировка (intrinsic lock), типа его личная дубинка. И когда поток заходит в synchronized метод или блок, он эту дубинку хватает и кричит: «Моё!». Все остальные потоки, которые тоже хотят зайти в синхронизированные места, защищённые этой же самой дубинкой, стоят и ждут, как лохи, пока первый не набегается и не положит её на место.

Как этим пользоваться? Да по-разному, блядь!

  1. Синхронизированный метод. Самый простой способ, для ленивых. Весь метод обносится колючкой. Для обычного метода дубинка — это сам объект (this), а для статического — вообще класс целиком (Class объект). Грубо, но работает.

    public synchronized void increment() {
        counter++; // Теперь тут можно спокойно работать, никто не влезет
    }
  2. Синхронизированный блок. А это уже для тех, кто похитрее. Тут ты сам указываешь, какую конкретно дубинку хватать. Можно взять не всего себя (this), а какую-то отдельную палку. Это часто эффективнее, потому что блокируешь не весь дом, а только один сортир.

    private final Object lock = new Object(); // Вот она, наша отдельная дубинка
    private int counter;
    
    public void increment() {
        // Вся остальная фигня в методе может работать без блокировки
        // А вот этот кусочек — только с нашей дубинкой в руках
        synchronized(lock) {
            counter++;
        }
    }

А теперь самое важное, про что все забывают — happens-before, ёпта! Когда поток отпускает блокировку, он как бы кричит всем остальным: «Смотрите сюда, мудаки! Все изменения, которые я тут натворил в синхронизированном блоке, теперь официально вступают в силу!». И любой другой поток, который потом схватит эту же самую блокировку, гарантированно увидит все эти изменения. Никаких там кешей процессорных и перестановок команд — всё чётко, как по нотам. Это решает кучу проблем с видимостью, которые возникают просто на ровном месте.

Но не всё так гладко, конечно:

  • Deadlock (Взаимная блокировка). Это когда два потока схватили по дубинке и ждут, пока другой отпустит свою, чтобы взять её. И стоят так до скончания времён. Классика жанра, пиздец.
  • Производительность. Хватать и отпускать дубинки — это не бесплатно. Если делать это каждую миллисекунду, можно всю скорость угробить.
  • Есть же альтернативы, блядь! Не надо всё на synchronized вешать. В арсенале java.util.concurrent целая куча инструментов для взрослых: ReentrantLock (блокировка с доп. фичами), Semaphore (пропуск в ночной клуб), атомарные штуки типа AtomicInteger (меняй значение без всяких блокировок) и потоко-безопасные коллекции вроде ConcurrentHashMap. Ими и пользуйся, когда обычной дубинки уже мало.