Java: кэширование энергонезависимых переменных различными потоками

Ситуация такова:

  • У меня есть объект с множеством сеттеров и геттеров.
  • Экземпляр этого объекта создается в одном конкретном потоке, где установлены все значения. Сначала я создаю "пустой" объект, используя новый оператор, и только тогда я вызываю некоторые методы сеттеров на основе некоторой сложной логики.
  • Только тогда этот объект стал доступен для всех других потоков, которые используют только геттеры.

Вопрос: Должен ли я сделать все переменные этого класса неустойчивыми или нет?

Заботы:

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

Примечание. Я знаю о шаблоне построителя, но я не могу применить его там по нескольким причинам: (

Редакция: Поскольку я чувствую, что два ответа от Mathias и axtavt не совпадают очень хорошо, я хотел бы добавить пример:

Скажем, у нас есть класс foo:

class Foo {   
    public int x=0;   
}

и два потока используют его, как описано выше:

 // Thread 1  init the value:   
 Foo f = new Foo();     
 f.x = 5;     
 values.add(f); // Publication via thread-safe collection like Vector or Collections.synchronizedList(new ArrayList(...)) or ConcurrentHashMap?. 

// Thread 2
if (values.size()>0){        
   System.out.println(values.get(0).x); // always 5 ?
}

Как я понял, Матиас, он может распечатать 0 на некоторых JVM в соответствии с JLS. Поскольку я понял axtavt, он всегда будет печатать 5.

Каково ваше мнение?

- С Уважением, Дмитрий

Ответ 1

В этом случае вам нужно использовать безопасные идиомы публикации, когда ваш объект доступен для других потоков, а именно (от Java Concurrency на практике):

  • Инициализация ссылки на объект из статического инициализатора;
  • Сохранение ссылки на него в поле volatile или AtomicReference;
  • Сохранение ссылки на него в конечное поле правильно построенного объекта; или
  • Сохранение ссылки на него в поле, которое должным образом защищено блокировкой.

Если вы используете безопасную публикацию, вам не нужно объявлять поля volatile.

Однако, если вы его не используете, объявление полей volatile (теоретически) не поможет, поскольку барьеры памяти, вызванные volatile, являются односторонними: volatile write может быть после него переупорядочивается с помощью энергонезависимых действий.

Итак, volatile обеспечивает правильность в следующем случае:

class Foo {
    public int x;
}
volatile Foo foo;

// Thread 1
Foo f = new Foo();
f.x = 42;
foo = f; // Safe publication via volatile reference

// Thread 2
if (foo != null)
     System.out.println(foo.x); // Guaranteed to see 42

но не работают в этом случае:

class Foo {
    public volatile int x;
}
Foo foo;

// Thread 1
Foo f = new Foo();
// Volatile doesn't prevent reordering of the following actions!!!
f.x = 42;
foo = f;

// Thread 2
if (foo != null)
     System.out.println(foo.x); // NOT guaranteed to see 42, 
                                // since f.x = 42 can happen after foo = f

С теоретической точки зрения, в первом образце существует транзитивное событие-до отношения

f.x = 42 happens before foo = f happens before read of foo.x 

Во втором примере f.x = 42 и чтение foo.x не связаны взаимозависимостью-before, поэтому они могут выполняться в любом порядке.

Ответ 2

Вам не нужно объявлять, что поле volatile его значения установлено до того, как метод start вызывается в потоках, которые читают поле.

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

Соответствующие правила из JLS:

  • Каждое действие в потоке происходит - перед каждым действием в этом потоке, которое приходит позже в порядке программы
  • Запускается вызов для начала в потоке - перед любым действием в запущенном потоке.

Однако, если вы запускаете другие потоки перед установкой поля, вы должны объявить поле volatile. JLS не позволяет предположить, что нить не будет кэшировать значение до того, как оно прочитает его впервые, даже если это может быть в случае конкретной версии JVM.

Ответ 3

Чтобы полностью понять, что происходит, я читал о модели памяти Java (JMM). Полезное введение в JMM можно найти в Java Conurrency на практике.

Я думаю, что ответ на вопрос: да, в приведенном примере, чтобы члены объекта volatile НЕ НЕОБХОДИМО. Тем не менее, эта реализация довольно хрупкая, поскольку эта гарантия зависит от точного ЗАКАЗА, в котором делаются вещи, и от безопасности потоков контейнера. Шаблон компоновщика будет намного лучшим вариантом.

Почему это гарантировано:

  • В потоке 1 выполняется все назначение, прежде чем помещать значение в контейнер с потоком.
  • В методе добавления контейнера, защищенного потоком, необходимо использовать некоторую конструкцию синхронизации, такую ​​как изменчивое чтение/запись, блокировка или синхронизация(). Это гарантирует две вещи:

    • Инструкции, которые находятся в потоке 1. до того, как синхронизация будет выполняться ранее. Этому JVM не разрешается изменять порядок инструкций для целей оптимизации с помощью инструкции синхронизации. Это называется случаем - перед гарантией.
    • Все записи, которые происходят до того, как синхронизация в потоке 1 будет впоследствии видна для всех остальных потоков.
  • Объекты НИКОГДА не изменяются после публикации.

Однако, если контейнер не был потокобезопасным или порядок вещей был изменен кем-то, кто не знал об этом шаблоне, или объекты случайно меняются после публикации, то больше нет гарантий. Таким образом, после шаблона Builder, который может быть создан Google AutoValue или Freebuilder, гораздо безопаснее.

Эта статья на эту тему также неплоха: http://tutorials.jenkov.com/java-concurrency/volatile.html