Java Concurrency на практике - Пример 14.12 - программирование
Подтвердить что ты не робот

Java Concurrency на практике - Пример 14.12

// Not really how java.util.concurrent.Semaphore is implemented
@ThreadSafe
public class SemaphoreOnLock {
    private final Lock lock = new ReentrantLock();
    // CONDITION PREDICATE: permitsAvailable (permits > 0)
    private final Condition permitsAvailable = lock.newCondition();
    @GuardedBy("lock") private int permits;

    SemaphoreOnLock(int initialPermits) {
        lock.lock();
        try {
            permits = initialPermits;
        } finally {
            lock.unlock();
        }
    }

/* other code omitted.... */

У меня есть вопрос о примере выше, который извлекается из Java Concurrency в Практике. Листинг 14.12. Считывание семафора с использованием блокировки.

Мне интересно, почему нам нужно получить блокировку в конструкторе (как показано, вызывается lock.lock()). Насколько я знаю, конструктор является атомарным (кроме ссылки экранированным), поскольку ни один другой поток не может получить ссылку, следовательно, полуконструированный объект не будет виден другим потокам. Поэтому для конструкторов нам не нужен синхронный модификатор. Кроме того, нам не нужно беспокоиться о видимости памяти, если объект безопасно опубликован.

Итак, зачем нам нужен объект ReentrantLock внутри конструктора?

4b9b3361

Ответ 1

полупостроенный объект не виден другим нитям

Это неверно. Объект видим другим потокам во время построения, если он имеет любые не конечные/изменчивые поля. Поэтому другие потоки могут видеть значение по умолчанию для permits i.e 0, которое может не соответствовать текущему потоку.

Модель памяти Java предлагает специальную гарантию безопасности инициализации для неизменяемых объектов (объект с только конечными полями). Ссылка на объект, видимая для другого потока, не обязательно означает, что состояние этого объекта видимо для потребляющего потока - JCP $3.5.2

Из листинга 3.15 Java Concurrency на практике:

Хотя может показаться, что значения полей, заданные в конструкторе, являются первые значения, записанные в эти поля, и, следовательно, нет "старшие" значения, чтобы увидеть как устаревшие значения, конструктор Объектзаписывает значения по умолчанию во все поля перед конструкторами подкласса запустить. Поэтому можно увидеть значение по умолчанию для поля в качестве a устаревшее значение.

Ответ 2

Я честно не вижу никакого действительного использования для блокировки здесь, кроме того, что он вводит забор памяти. int назначения в любом случае являются атомарными на 32/64 бит.

Ответ 3

(Просто уточняю это для моей собственной бедной головы - другие ответы верны).

Экземпляры этого гипотетического класса SemaphoreOnLock предназначены для совместного использования. Итак, thread T1 полностью конструирует экземпляр и помещает его где-нибудь, где thread T2 может видеть его и вызывать некоторый метод, который требует чтения поля permits. Некоторые важные моменты, относящиеся к полю permits:

  • он инициализируется в первом случае значением по умолчанию 0
  • ему присваивается значение (которое может отличаться от значения по умолчанию 0), потоком T1
  • это не volatile
  • это не final (что делает его вроде "одним выстрелом волатильным" )

Поэтому, если мы хотим, чтобы T2 читал значение, которое T1 написал в последний раз, нам нужно синхронизировать. Мы должны сделать это в конструкторе, как и в любом другом случае. (Тот факт, что это присвоение атома или нет, не влияет на эту проблему видимости). Стратегия ограничения построенного SemaphoreOnLock для одного потока не работает для нас, потому что вся идея сделать это @Threadsafe заключается в том, что мы можем смело делиться ею.

Что иллюстрирует этот пример, так это то, что "быть потокобезопасным" относится также к построению объекта при установке любого нестатического, не конечного, энергонезависимого поля на значение, отличное от его значения по умолчанию.

Конечно, мы не обязаны думать об этом, когда у нас есть класс @NotThreadsafe. Если вызывающий абонент строит нас и решает поделиться нами между двумя потоками, то вызывающий должен организовать соответствующую синхронизацию. В этом сценарии мы можем делать все, что угодно, в конструкторе, не беспокоясь о проблемах видимости - что-то еще проблема.