Является ли незанятая ленивая модель загрузки, используемая в Guava, действительно потокобезопасной?

Некоторые внутренние типы Guava, такие как AbstractMultiset, имеют такой шаблон:

private transient Set<E> elementSet;

@Override
public Set<E> elementSet() {
  Set<E> result = elementSet;
  if (result == null) {
    elementSet = result = createElementSet();
  }
  return result;
}

Set<E> createElementSet() {
  return new ElementSet();
}

Идея состоит в том, чтобы отложить создание представлений коллекции (elementSet(), entrySet()), пока они не понадобятся. Там нет блокировки вокруг процесса, потому что, если два потока звонят elementSet() в одно и то же время, вполне можно вернуть два разных значения. Будет полезна запись поля elementSet, но поскольку записи в ссылочные поля всегда являются атомарными в Java, неважно, кто победит в гонке.

Тем не менее, я беспокоюсь о том, что говорит модель памяти Java о inlining здесь. Если конструкторы createElementSet() и elementSet становятся вложенными, кажется, что мы могли бы получить что-то вроде этого:

@Override
public Set<E> elementSet() {
  Set<E> result = elementSet;
  if (result == null) {
    elementSet = result = (allocate an ElementSet);
    (run ElementSet constructor);
  }
  return result;
}

Это позволит другому потоку наблюдать непустое, но не полностью инициализированное значение для elementSet. Есть ли причина, которая не может произойти? Из моего чтения JLS 17.5 кажется, что другие потоки гарантируют только правильные значения для полей final в elementSet, но поскольку elementSet в конечном итоге происходит от AbstractSet, я не думаю, что есть гарантия, что все его поля final.

Ответы

Ответ 1

Я не уверен на 100% (я уверен, что кто-то из нашей команды мог бы ответить на это лучше). Тем не менее, пару мыслей:

  • Я не думаю, что мы требуем, чтобы это было (гарантировано) потокобезопасным. Непотопляемые коллекции, такие как HashMultiset, расширяют AbstractMultiset. Тем не менее, ConcurrentHashMultiset также расширяет AbstractMultiset и использует его реализацию elementSet(), поэтому, по-видимому, на самом деле он может быть потокобезопасным.
  • Я уверен, что безопасность потока этого метода зависит от реализации createElementSet(). Из того, что я могу сказать, если Set, созданный createElementSet(), неизменен (в том, что поля, которые назначаются при его построении, final), он должен быть потокобезопасным. Это, по-видимому, верно в случае ConcurrentHashMultiset по крайней мере.

Изменить: Я спросил об этом Джереми Мэнсона, и он сказал: "Твоя помощь мне кажется прекрасной. Это не потокобезопасно. Если построенный объект имеет все окончательные поля в правильных местах, вы должны быть в порядке, но я не стал бы полагаться на это случайно (обратите внимание, что многие реализации фактически неизменяемы, а не искренне неизменяемы).

Примечание. Для поточно-безопасных коллекций, таких как ConcurrentHashMultiset, которые используют этот шаблон, созданные объекты преднамеренно и по-настоящему неизменяемы (хотя если бы AbstractSet изменилось, это могло бы измениться, как заметил Крис в комментариях).