Проверьте, будут ли вызовы increment и reset взаимно исключаться для одного объекта Counter при работе из р...

Проверьте, будут ли вызовы increment() и reset() взаимно исключаться для одного объекта Counter при работе из разных потоков.

class Counter {
    private int value;

    synchronized void increment() {
        value++;
    }

    void reset() {
        synchronized (this) {
            value = 0;
        }
    }
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Да. Для одного экземпляра Counter методы increment() и reset() используют один и тот же монитор — монитор объекта this, поэтому их тела не выполняются одновременно для разных потоков.

Однако это верно только для обращений к этому же объекту и только в тех участках, где используется тот же монитор. Доступ к value в обход синхронизации по-прежнему может привести к гонкам.

Исторический контекст

Конструкция synchronized — встроенный механизм Java для защиты общего изменяемого состояния с помощью мониторов объектов. Она решает задачу координации потоков без необходимости вручную реализовывать блокировки и протоколы ожидания.

Монитор объединяет две гарантии: взаимное исключение и правила видимости изменений между потоками. Это сделало синхронизацию частью базовой семантики языка, а не только возможностью библиотек.

Постановка проблемы

Операция value++ состоит как минимум из чтения значения, вычисления нового значения и записи результата. Если два потока выполняют её одновременно без синхронизации, один результат может затереть другой.

Сброс значения также конфликтует с увеличением: поток может прочитать старое значение до reset(), а записать новое уже после него. Поэтому важно защищать связанные операции одним и тем же монитором.

Подробное решение

Синхронизированный нестатический метод фактически использует монитор текущего объекта. В данном случае объявление метода эквивалентно синхронизации блока по this:

class Counter { private int value; void increment() { synchronized (this) { value++; } } void reset() { synchronized (this) { value = 0; } } }

Поэтому increment() и reset() одного объекта конкурируют за один монитор. Пока один поток владеет монитором, другой ждёт его освобождения; после выхода из синхронизированного участка изменения становятся видимыми следующему потоку, успешно получившему тот же монитор.

Гарантия не распространяется на разные экземпляры: counter1.increment() и counter2.reset() используют разные мониторы. Также synchronized не делает автоматически безопасными чтения и записи, выполненные без синхронизации.

Синхронизация по this удобна, но объект доступен внешнему коду, который может захватить его монитор и создать нежелательную конкуренцию или взаимную блокировку. Для класса с более строгим контролем обычно применяют отдельный приватный объект-блокировку или java.util.concurrent.locks.Lock.

Мониторы Java реентерабельны: поток, уже владеющий монитором объекта, может повторно войти в другой синхронизированный участок по тому же объекту. Другой поток при этом всё равно должен ждать.

Ситуация из практики

В кэше есть методы put() и clear(), синхронизированные по this. Сначала это работает, но затем внешний код начинает синхронизироваться на экземпляре кэша для собственных задач. Такой общий монитор увеличивает связанность и может привести к блокировкам, которые трудно диагностировать.

Вариант с synchronized-методами прост и хорошо подходит для небольшого класса: он легко читается и автоматически освобождает монитор при выходе, включая выход через исключение. Недостаток — невозможность отделить внутреннюю блокировку от публичного объекта.

Вариант с приватным объектом lock лучше изолирует внутреннюю синхронизацию, но требует явно поддерживать единое правило доступа к состоянию. В практическом решении выбирают приватную блокировку, если экземпляр доступен внешнему коду; это уменьшает риск случайного вмешательства и сохраняет взаимное исключение только для внутреннего состояния.

Что кандидаты часто упускают

  1. Вопрос: Что изменится, если increment() сделать static synchronized?

    Ответ: Метод будет синхронизироваться не на экземпляре, а на объекте класса Counter.class. Он будет конкурировать с другими static synchronized-методами Counter, но не с обычными синхронизированными методами экземпляра, которые используют this.

  2. Вопрос: Достаточно ли синхронизировать только запись value, оставив чтение без блокировки?

    Ответ: Нет, если требуется согласованное и своевременно видимое значение. Несинхронизированное чтение может не иметь требуемой гарантии видимости и может наблюдать состояние, не согласованное с другими операциями. Для общего протокола доступа чтение и запись должны использовать тот же монитор либо другой корректный механизм синхронизации.

  3. Вопрос: Гарантирует ли synchronized отсутствие взаимной блокировки?

    Ответ: Нет. Он предотвращает одновременное выполнение критических секций по одному монитору, но взаимная блокировка возможна при захвате нескольких мониторов в разном порядке. Например, один поток может удерживать lockA и ждать lockB, а другой — удерживать lockB и ждать lockA. Для снижения риска используют единый порядок захвата блокировок, уменьшают область блокировки или применяют механизмы с ограниченным ожиданием.