Java: безопасно "утечка" этой ссылки в конструкторе для конечного класса через отношение _happens-before_?

Раздел 3.2.1 Goetz "Java Concurrency in Practice" содержит следующее правило:

Не разрешайте ссылку this уйти во время построения

Я понимаю, что в общем случае разрешение this на выход может привести к тому, что другие потоки будут видеть неполностью сконструированные версии вашего объекта и нарушают гарантию безопасности инициализации полей final (как обсуждалось, например, здесь)

Но можно ли безопасно протекать this? В частности, если вы установили связь happen-before до утечки?

Например, официальный исполнитель Javadoc говорит

Действия в потоке перед отправкой объекта Runnable в Executor произойдет - до его запуска, возможно, в другом потоке

Мое наивное понимание чтения модели памяти Java заключается в том, что что-то вроде следующего должно быть безопасным, даже если оно протекает this до конца конструктора:

public final class Foo {
  private final String str1;
  private String str2;
  public Foo(Executor ex) {
    str1 = "I'm final";
    str2 = "I'm not";
    ex.execute(new Runnable() {
      // Oops: Leakage!
      public void run() { System.out.println(str1 + str2);}
    });
  }
}

То есть, несмотря на то, что мы пропустили this потенциально злонамеренный Executor, назначения перед str1 и str2 произойдут - до утечки, поэтому объект полностью (целиком и полностью) построенный, хотя он не был "полностью инициализирован" на JLS 17.5.

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

Я что-то упустил? Действительно ли это гарантировано, что он хорошо себя ведет? Он выглядит как законный пример "Piggybacking on synchronization" (16.1.4). В целом я был бы очень признателен за любые указания на дополнительные ресурсы, в которых эти проблемы будут рассмотрены.

EDIT. Я знаю, что, как заметил @jtahlborn, я могу избежать проблемы с помощью публичного статического factory. Я ищу ответ на вопрос непосредственно, чтобы укрепить мое понимание модели памяти Java.

EDIT # 2: Этот ответ ссылается на то, к чему я пытаюсь добраться. То есть, следуя правилу из приведенного там JLS, достаточно для обеспечения видимости всех полей final. Но нужно ли это, или мы можем использовать другие механизмы, чтобы обеспечить наши гарантии на видимость?

Ответ 1

Вы правы. В общем, модель памяти Java не обрабатывает конструкторы каким-либо особым образом. Публикация ссылки на объект до или после выхода конструктора делает очень мало различий.

Единственное исключение, конечно, относится к полям final. Выход конструктора, где записывается окончательное поле, для определения действия "замораживания" в поле; если this публикуется после freeze, даже если это не происходит - перед ребрами другие потоки будут корректно инициализировать поле; но не если this опубликовано до freeze.

Интересно, что если существует цепочка конструкторов, freeze определяется в наименьшей области; например.

-- class Bar

final int x;

Bar(int x, int ignore)
{
    this.x = x;  // assign to final
}  // [f] freeze action on this.x

public Bar(int x)
{ 
    this(x, 0);
    // [f] is reached!
    leak(this); 
}

Здесь leak(this) является безопасным w.r.t. this.x.

Подробнее см. в разделе для более подробной информации о полях final.


Если final кажется слишком сложным, это так. Мой совет - забудьте об этом! Никогда не полагайтесь на полевую семантику final, чтобы опубликовать ее без предупреждения. Если программа правильно синхронизирована, вам не нужно беспокоиться о полях final или их нежной семантике. К сожалению, нынешний климат заключается в том, чтобы максимально продвигать поля final, создавая чрезмерное давление на программистов.