Безопасная публикация, когда значения считываются синхронными методами

Мой вопрос касается безопасной публикации значений полей в Java (как описано здесь многопоточность и безопасная публикация Java).

Как я понимаю, поле можно безопасно читать (это означает, что доступ из нескольких потоков будет видеть правильное значение), если:

  • чтение и запись синхронизируются на одном мониторе Поле
  • является окончательным. Поле
  • нестабильно

Если мое понимание верное, следующий класс должен не быть потокобезопасным, так как начальное значение записывается без этих характеристик. Однако мне трудно поверить, что мне нужно сделать first volatile, хотя он доступен только из метода synchronized.

public class Foo {

    private boolean needsGreeting = true;

    public synchronized void greet() {
        if (needsGreeting) {
            System.out.println("hello");
            needsGreeting = false;
        }
    }
}

Я что-то упустил? Правильно ли указан код и если да, то почему? Или необходимо в таких случаях сделать first volatile или использовать final AtomicBoolean или что-то вроде этого дополнительно для доступа к нему из метода synchronized.

(Чтобы уточнить, я знаю, что если начальное значение было записано в методе synchronized, оно было бы потокобезопасным даже без ключевого слова volatile.)

Ответ 1

Между обработчиком конструктора и вызовами метода не происходит - до того, как один поток может начать создание экземпляра и сделать ссылку доступной, а другой поток - получить эту ссылку и начать вызов метод greet() на частично построенном объекте. Синхронизация в greet() на самом деле не затрагивает эту проблему.

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

public class Foo {
    private boolean needsGreeting = true;

    public synchronized void greet() {
        if (needsGreeting) {
            System.out.println("Hello.");
            needsGreeting = false;
        }
    }
}

class FooUser {
    private static Foo foo;

    public static Foo getFoo() {
        if (foo == null) {
            synchronized (FooUser.class) {
                if (foo == null) {
                    foo = new Foo();
                }
            }
        }
        return foo;
    }
}

Если несколько потоков одновременно вызывают FooUser.getFoo(). greet(), один поток может создавать экземпляр Foo, но другой поток может преждевременно найти ненулевую ссылку Foo и вызвать greet() и найти needsGreeting все равно false.

Пример этого упоминается в Java Concurrency в Практике (3.5).

Ответ 2

Строго говоря, я не вижу, чтобы можно было с уверенностью предположить, что needsGreeting устанавливается в true, когда вызывается greet.

Для того, чтобы это было правдой, должно было бы произойти перед отношением между начальной записью (происходящей, когда объект создается) и первым чтением (в greet -method). Глава 17 Потоки и блокировки в JLS, однако, гласит следующее об ограничениях "происходит до (hb)":

17.4.5 Бывает до заказа Два действия могут быть упорядочены отношениями "происходит до". Если одно действие происходит раньше другого, то первое видно и упорядочено до второго.

Если у нас есть два действия x и y, мы пишем hb (x, y), чтобы указать, что x происходит до y.

  • Если x и y являются действиями одного и того же потока, а x предшествует y в программном порядке, то hb (x, y).
  • Для этого объекта есть крайний случай "до появления" от конца конструктора объекта до начала финализатора (§12.6).
  • Если действие x синхронизируется со следующим действием y, то у нас также есть hb (x, y).
  • Если hb (x, y) и hb (y, z), то hb (x, z).

Кроме того, единственный способ ввести отношение с синхронизацией, то есть порядок синхронизации, состоит в том, чтобы сделать что-то из следующего:

Синхронизирующие действия вызывают синхронизированное отношение к действиям, определенное следующим образом:

  • Действие разблокировки на мониторе m синхронизируется со всеми последующими действиями блокировки на m (где последующие определяются в соответствии с порядком синхронизации).
  • Запись в энергозависимую переменную (§8.3.1.4) v синхронизирует со всеми последующими чтениями v любым потоком (где последующие определены в соответствии с порядком синхронизации).
  • Действие, которое запускает поток, синхронизируется с первым действием в потоке, которое он запускает.
  • Запись значения по умолчанию (ноль, ложь или ноль) в каждую переменную синхронизируется с первым действием в каждом потоке. Хотя может показаться немного странным записать значение по умолчанию в переменную до того, как будет выделен объект, содержащий переменную, концептуально каждый объект создается в начале программы с его инициализированными значениями по умолчанию.
  • Последнее действие в потоке T1 синхронизируется с любым действием в другом потоке T2, которое обнаруживает, что T1 завершен. T2 может сделать это, вызвав T1.isAlive() или T1.join().
  • Если поток T1 прерывает поток T2, прерывание по T1 синхронизируется с любой точкой, где любой другой поток (включая T2) определяет, что T2 был прерван (с помощью брошенного InterruptedException или путем вызова Thread.interrupted или Thread.isInterrupted).

Нигде не говорится, что "конструирование объекта происходит до любых вызовов методов объекта. Однако отношение" происходит до "утверждает, что существует край" происходит до "от конца конструктора объекта до начала объекта. финализатор (§12.6) для этого объекта. Это может быть намек на то, что не существует края, предшествующего событию, от конца конструктора объекта до начала произвольного метода!