Окончательное поле, ссылка и безопасная публикация

Рассмотрим следующую нетрадиционную реализацию блокировки с двойной проверкой, которая не использует volatile:

public class ValueProvider {
  private static State state = new Initial();

  public static Value getValue() {
      return state.getValue();
  }

  private static class Initial implements State {
      @Override
      public synchronized Value getValue() {
          if (state instanceof Initial) {
              Value value = new Value();
              value.x = 1;
              value.y = 2; 
              state = new Initialized(value);
              return value;
          } else {
              return state.getValue();
          }
      }
  }

  private static class Initialized implements State {
      private final Value value;

      private Initialized(Value value) {
          this.value = value;
      }

      @Override
      public Value getValue() {
          return value;
      }
  }

  private interface State {
      Value getValue();
  }

  public static final class Value {
      private int x;
      private int y;

      public int getX() {
          return x;
      }

      public int getY() {
          return y;
      }
  }

}

Является ли этот код потокобезопасным?

В частности, я спрашиваю о конечном поле и гарантиях, которые оно дает, поэтому вопрос можно переформулировать так: возможно ли, чтобы какой-то поток получил неинициализированный экземпляр Value ?

ОБНОВЛЕНИЕ: удалено упоминание о сеттерах, так что после публикации доступны только чтения


person AngryJuice    schedule 04.07.2014    source источник
comment
Рассмотрим этот гораздо более простой и надежный подход к ленивой инициализации.   -  person Bohemian♦    schedule 04.07.2014
comment
@Bohemian хороший подход; просто будьте осторожны с неперехваченными исключениями на этапе инициализации, и вы золоты.   -  person Chris K    schedule 04.07.2014
comment
@Bohemian Спасибо, но ключевой момент здесь не в том, чтобы найти лучший подход к ленивой инициализации, а в том, чтобы проиллюстрировать принцип безопасной публикации с использованием ключевого слова final. Хотя это определенно верно для простых случаев, таких как примитивные атрибуты в конструкторе, вопрос в том, будет ли это работать в более сложных случаях, подобных описанным.   -  person AngryJuice    schedule 04.07.2014


Ответы (2)


Ну, помимо того факта, что ваш подход слишком сложен, как указал Bohemian ♦, это может сработать в отношении публикации. Если два потока обращаются к getValue() одновременно, только один поток может войти в блок synchronized. Другой будет либо заблокирован в блоке synchronized, либо увидит экземпляр Initialized с правильно инициализированным полем value из-за гарантии инициализации поля final.

Однако это по-прежнему не работает, потому что экземпляр класса Value является изменяемым, а ваш комментарий // getters and setters указывает, что экземпляр будет изменен после построения. В этом случае вся гарантия инициализации поля final бессмысленна, поскольку класс Value не является потокобезопасным. Вы можете быть защищены от просмотра значений по умолчанию для x и y, но вы никогда не узнаете, какие значения в отношении более поздних изменений вы увидите, а значения для (x, y) не обязательно совпадают.

person Holger    schedule 04.07.2014
comment
Хорошая точка зрения. Но давайте рассмотрим, что мы удаляем публичные сеттеры и оставляем только геттеры (поэтому после публикации доступны только чтения). Будет ли ключевое слово final гарантировать безопасную публикацию или оно будет гарантировать только то, что другие потоки не увидят экземпляр Initialized со значением = null, но само значение может быть не полностью сконструировано (например, с x = 1, y = 0)? - person AngryJuice; 04.07.2014
comment
Гарантия безопасности final распространяется на все объекты, достижимые через эту ссылку final, при условии отсутствия изменений после построения и, конечно же, нет доступа к этим объектам через другие ссылки. В вашем случае вы увидите полностью построенный объект Value, включающий правильные значения для x и y. Ярким примером, который полагается на эту гарантию, является java.lang.String, который не может объявить элементы своего внутреннего массива char[] неизменяемыми. Но все элементы всегда считываются через ссылку на массив final - person Holger; 04.07.2014

Нет, это не потокобезопасно. Нет барьера памяти при чтении ValueProvider.state и вообще нет барьера для Value.

Эмпирическое правило параллелизма Java заключается в том, что должен быть барьер памяти при чтении и барьер памяти при записи.

Единственные способы добавить барьер памяти в Java:

  • синхронизированный
  • изменчивый
  • инициализация класса (неявно в jvm)
  • атомный
  • небезопасный

В большинстве случаев Hotspot игнорирует финальное ключевое слово и предпочитает выводить его самостоятельно. Однако, когда final действительно влияет на JMM, это связано с созданием и встраиванием классов. Правила переупорядочивания полей final описаны в поваренной книге, о которой вы уже упоминали. В нем не упоминаются окончательные классы. В кулинарной книге указано:

Loads and Stores of final fields act as "normal" accesses 
with respect to locks and volatiles, but impose two additional reordering

1) Хранилище конечного поля (внутри конструктора и, если поле является ссылкой, любое хранилище, на которое может ссылаться этот финал, не может быть переупорядочено с последующим хранилищем

2) Первоначальная загрузка (т. е. самая первая встреча потока) конечного поля не может быть переупорядочена с начальной загрузкой ссылки на объект, содержащий конечное поле.

person Chris K    schedule 04.07.2014
comment
Правильно, это означает, что вы не можете изменить порядок присваивания конечному полю и ссылке, но как насчет самого присвоенного значения? Можно ли переупорядочить действия с ним (x=1, y=2) по ссылке? - person AngryJuice; 04.07.2014
comment
@AngryJuice Был бы полезен небольшой пример Java, чтобы сделать этот вопрос о переупорядочении x и y со ссылкой более четкой. - person Chris K; 04.07.2014