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

Мой вопрос касается безопасной публикации значений полей в 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.)

Ответы [ 2 ]

5 голосов
/ 27 октября 2010

Между концом конструктора и вызовами методов не существует никакой связи «до того, как это произойдет», и поэтому один поток может начать конструирование экземпляра и сделать ссылку доступной, а другой поток получить эту ссылку и начать вызов.метод 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 in Practice (3,5).

3 голосов
/ 27 октября 2010

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

Чтобы это было правдой, пришлось быБыть до того, как возникнет связь между начальной записью (происходящей, когда объект создается) и первым чтением (в greet -методе). Глава 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 любым потоком (где последующие определяются в соответствии с синхронизациейorder).
  • Действие, которое запускает поток, синхронизируется с первым действием в потоке, которое он запускает.
  • Запись значения по умолчанию (ноль, ложь или ноль) для каждой переменной синхронизируется-с первым действием в каждой теме.Хотя может показаться немного странным записать значение по умолчанию в переменную до того, как будет выделен объект, содержащий переменную, концептуально каждый объект создается в начале программы с его инициализированными значениями по умолчанию.
  • Последнее действиев потоке T1 синхронизируется с любым действием в другом потоке T2, которое обнаруживает, что T1 завершен.T2 может выполнить это, вызвав T1.isAlive () или T1.join ().
  • Если поток T1 прерывает поток T2, прерывание по T1 синхронизируется с любой точкой, где любой другой поток (включая T2) определяет, чтоT2 был прерван (вызван InterruptedException или вызван Thread.interrupted или Thread.isInterrupted).

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...