Что даёт synchronized
synchronized решает сразу две задачи: взаимное исключение (в критической секции по данному монитору одновременно лишь один поток) и видимость (выход из монитора happens-before следующего входа, поэтому изменения видны другому потоку).
Монитор есть у каждого объекта. synchronized-метод берёт монитор this (или класса для static), а synchronized(obj)-блок — монитор obj. Видимость без взаимного исключения не работает: оба свойства даёт именно правильная синхронизация.
Reentrancy
Встроенные мониторы реентерабельны: поток, уже держащий монитор, может войти в другой synchronized-блок по тому же монитору без самоблокировки (счётчик удержаний увеличивается). Это позволяет synchronized-методу вызывать другой synchronized-метод того же объекта.
Без реентерабельности любой такой вложенный вызов был бы deadlock-ом самого с собой.
Guarded blocks: wait/notify
wait/notify/notifyAll координируют потоки по условию и вызываются только при удерживаемом мониторе. wait() отпускает монитор и засыпает, пока другой поток не вызовет notify по тому же монитору.
Два железных правила: проверять условие в цикле while (а не if) из-за ложных пробуждений (spurious wakeups) и общих notify, и предпочитать notifyAll, когда на мониторе ждут разные условия, иначе можно разбудить «не тот» поток и потерять сигнал.
synchronized (lock) {
while (!ready) { // while, не if — защита от spurious wakeup
lock.wait();
}
consume();
}
Что выбирать монитором
Нельзя синхронизироваться по общим/кэшируемым объектам: строковые литералы (интернируются), Integer в диапазоне кэша, Boolean — их разделяет весь JVM, и чужой код случайно заблокирует ту же ссылку. Синхронизация по this в публичном классе тоже рискованна: внешний код может взять ваш монитор.
Хорошая практика — приватный финальный объект-замок: private final Object lock = new Object();.
Deadlock, livelock, starvation
Deadlock — циклическое ожидание: поток A держит замок 1 и ждёт 2, поток B держит 2 и ждёт 1. Livelock — потоки активно реагируют друг на друга и не двигаются вперёд. Starvation — поток вечно не получает ресурс из-за приоритетов/нечестности.
Профилактика deadlock: единый глобальный порядок захвата замков, короткие критические секции, никакого внешнего IO/вызовов чужого кода под замком, и tryLock с таймаутом (явные Lock) вместо бесконечного ожидания.
Как отвечать на собеседовании
Скажите, что synchronized даёт и взаимное исключение, и видимость (happens-before через монитор), объясните реентерабельность, разберите wait/notify с правилом while-цикла и notifyAll, и назовите причины deadlock с профилактикой (единый порядок захвата). Бонус — почему нельзя лочиться по String/Integer/this.