Проверьте, будут ли вызовы increment() и reset() взаимно исключаться для одного объекта Counter при работе из разных потоков.
class Counter {
private int value;
synchronized void increment() {
value++;
}
void reset() {
synchronized (this) {
value = 0;
}
}
}
Да. Для одного экземпляра Counter методы increment() и reset() используют один и тот же монитор — монитор объекта this, поэтому их тела не выполняются одновременно для разных потоков.
Однако это верно только для обращений к этому же объекту и только в тех участках, где используется тот же монитор. Доступ к value в обход синхронизации по-прежнему может привести к гонкам.
Конструкция synchronized — встроенный механизм Java для защиты общего изменяемого состояния с помощью мониторов объектов. Она решает задачу координации потоков без необходимости вручную реализовывать блокировки и протоколы ожидания.
Монитор объединяет две гарантии: взаимное исключение и правила видимости изменений между потоками. Это сделало синхронизацию частью базовой семантики языка, а не только возможностью библиотек.
Операция value++ состоит как минимум из чтения значения, вычисления нового значения и записи результата. Если два потока выполняют её одновременно без синхронизации, один результат может затереть другой.
Сброс значения также конфликтует с увеличением: поток может прочитать старое значение до reset(), а записать новое уже после него. Поэтому важно защищать связанные операции одним и тем же монитором.
Синхронизированный нестатический метод фактически использует монитор текущего объекта. В данном случае объявление метода эквивалентно синхронизации блока по this:
Поэтому increment() и reset() одного объекта конкурируют за один монитор. Пока один поток владеет монитором, другой ждёт его освобождения; после выхода из синхронизированного участка изменения становятся видимыми следующему потоку, успешно получившему тот же монитор.
Гарантия не распространяется на разные экземпляры: counter1.increment() и counter2.reset() используют разные мониторы. Также synchronized не делает автоматически безопасными чтения и записи, выполненные без синхронизации.
Синхронизация по this удобна, но объект доступен внешнему коду, который может захватить его монитор и создать нежелательную конкуренцию или взаимную блокировку. Для класса с более строгим контролем обычно применяют отдельный приватный объект-блокировку или java.util.concurrent.locks.Lock.
Мониторы Java реентерабельны: поток, уже владеющий монитором объекта, может повторно войти в другой синхронизированный участок по тому же объекту. Другой поток при этом всё равно должен ждать.
В кэше есть методы put() и clear(), синхронизированные по this. Сначала это работает, но затем внешний код начинает синхронизироваться на экземпляре кэша для собственных задач. Такой общий монитор увеличивает связанность и может привести к блокировкам, которые трудно диагностировать.
Вариант с synchronized-методами прост и хорошо подходит для небольшого класса: он легко читается и автоматически освобождает монитор при выходе, включая выход через исключение. Недостаток — невозможность отделить внутреннюю блокировку от публичного объекта.
Вариант с приватным объектом lock лучше изолирует внутреннюю синхронизацию, но требует явно поддерживать единое правило доступа к состоянию. В практическом решении выбирают приватную блокировку, если экземпляр доступен внешнему коду; это уменьшает риск случайного вмешательства и сохраняет взаимное исключение только для внутреннего состояния.
Вопрос: Что изменится, если increment() сделать static synchronized?
Ответ: Метод будет синхронизироваться не на экземпляре, а на объекте класса Counter.class. Он будет конкурировать с другими static synchronized-методами Counter, но не с обычными синхронизированными методами экземпляра, которые используют this.
Вопрос: Достаточно ли синхронизировать только запись value, оставив чтение без блокировки?
Ответ: Нет, если требуется согласованное и своевременно видимое значение. Несинхронизированное чтение может не иметь требуемой гарантии видимости и может наблюдать состояние, не согласованное с другими операциями. Для общего протокола доступа чтение и запись должны использовать тот же монитор либо другой корректный механизм синхронизации.
Вопрос: Гарантирует ли synchronized отсутствие взаимной блокировки?
Ответ: Нет. Он предотвращает одновременное выполнение критических секций по одному монитору, но взаимная блокировка возможна при захвате нескольких мониторов в разном порядке. Например, один поток может удерживать lockA и ждать lockB, а другой — удерживать lockB и ждать lockA. Для снижения риска используют единый порядок захвата блокировок, уменьшают область блокировки или применяют механизмы с ограниченным ожиданием.