Что гарантирует безопасность потоков в Guava ImmutableList?

Javadoc в ImmutableList Гуавы говорит, что у класса есть свойства ImmutableCollection Guava, одним из которых является безопасность потоков:

Поток безопасности. Доступ к этой коллекции безопасен одновременно из нескольких потоков.

Но посмотрите на то, какImmutableListпостроен его строителем -Builderдержит все элементы в Object[] (это нормально, так как никто не сказал, что построитель является потокобезопасным) и при построении передает этот массив (или, возможно, копию) конструктору RegularImmutableList:

public abstract class ImmutableList<E> extends ImmutableCollection<E>
implements List<E>, RandomAccess {
    ...
    static <E> ImmutableList<E> asImmutableList(Object[] elements, int length) {
      switch (length) {
        case 0:
          return of();
        case 1:
          return of((E) elements[0]);
        default:
          if (length < elements.length) {
            elements = Arrays.copyOf(elements, length);
          }
          return new RegularImmutableList<E>(elements);
      }
    }
    ...
    public static final class Builder<E> extends ImmutableCollection.Builder<E> {
        Object[] contents;
        ...
        public ImmutableList<E> build() { //Builder's build() method
          forceCopy = true;
          return asImmutableList(contents, size);
        }
        ...
    }

}

Что значитRegularImmutableListделать с этими элементами? То, что вы ожидаете, просто инициирует свой внутренний массив, который затем используется для всех операций чтения:

class RegularImmutableList<E> extends ImmutableList<E> {
    final transient Object[] array;

    RegularImmutableList(Object[] array) {
      this.array = array;
    }

    ...
}

Как это может быть потокобезопасным? Что гарантирует связь междузаписью, выполняемой в Builderичитает изRegularImmutableList?

Согласно модели памяти Java, связь между событиями и событиями происходит только в пяти случаях (из Javadoc для java.util.concurrent):

  • Каждое действие в потоке происходит перед каждым действием в этом потоке, которое происходит позже в порядке программы.
  • Разблокировка (синхронизированный блок или выход метода) монитора происходит перед каждой последующей блокировкой (синхронизированный блок или вход метода) того же монитора. И поскольку отношение "происходит до" является транзитивным, все действия потока перед разблокировкой происходят до всех действий, следующих за блокировкой любого потока, отслеживающей этот процесс.
  • Запись в изменчивое поле происходит - перед каждым последующим чтением того же поля. Запись и чтение изменяемых полей имеют эффекты согласованности памяти, аналогичные входящим и выходящим мониторам, но не влекут за собой взаимную блокировку исключения.
  • Вызов для запуска в потоке происходит до любого действия в запущенном потоке.
  • Все действия в потоке происходят до того, как любой другой поток успешно вернется из соединения в этом потоке.

Ни один из них, кажется, не применим здесь Если какой-то поток строит список и передает свою ссылку некоторым другим потокам без использования блокировок (например, через final или же volatile поле), я не вижу, что гарантирует потокобезопасность. Что мне не хватает?

Редактировать:

Да, запись ссылки на массив является поточно-ориентированной, поскольку final, Так что это явно потокобезопасно. Что меня интересует, так это записи отдельных элементов. Элементы массива не являются ни final ни volatile, Все же они, кажется, написаны одним потоком и прочитаны другим без синхронизации.

Таким образом, вопрос можно свести к "если поток A пишет в final поле, это гарантирует, что другие потоки будут видеть не только эту запись, но и все предыдущие записи A?"

3 ответа

Решение

JMM гарантирует безопасную инициализацию (все значения, инициализированные в конструкторе, будут видны читателям), если все поля в объекте final и нет утечки this из конструктора 1:

class RegularImmutableList<E> extends ImmutableList<E> {

    final transient Object[] array;
      ^

    RegularImmutableList(Object[] array) {
        this.array = array;
    }
}

Последняя семантика поля гарантирует, что читатели увидят обновленный массив:

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


Спасибо @JBNizet и @chrylis за ссылку на JLS.

1 - "Если за этим следует, то, когда объект виден другим потоком, этот поток всегда будет видеть правильно построенную версию конечных полей этого объекта. Он также будет видеть версии любого объекта или массива, на которые ссылаются эти последние поля, которые являются по крайней мере так же актуально, как и последние поля. " - JLS §17.5.

Как вы заявили: "Каждое действие в потоке происходит - перед каждым действием в этом потоке, которое происходит позже в порядке программы".

Очевидно, что если поток мог каким-то образом получить доступ к объекту еще до того, как был вызван конструктор, вы бы облажались. Поэтому что-то должно препятствовать доступу к объекту до того, как его конструктор вернется. Но когда конструктор возвращается, все, что позволяет другому потоку обращаться к объекту, безопасно, потому что это происходит после в программном порядке конструирующего потока.

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

Поток это:

  1. Объект не существует и недоступен.
  2. Некоторые потоки вызывают конструктор объекта (или делают все остальное, что необходимо для подготовки объекта к использованию).
  3. Затем этот поток делает что-то, чтобы другие потоки могли получить доступ к объекту.
  4. Другие потоки теперь могут получить доступ к объекту.

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

Обратите внимание, что это применяется точно так же, если что-то должно быть сделано после возврата конструктора, вы можете просто считать их логически частью процесса построения. Точно так же части работы могут выполняться другими потоками, если все, что нужно для просмотра работы, выполненной другим потоком, не может начаться, пока не будет установлена ​​какая-либо связь с работой, которую сделал другой поток.

Разве это не 100% ответ на ваш вопрос?

Чтобы переформулировать:

Как это может быть потокобезопасным? Что гарантирует отношение "происходит до" между записями, выполняемыми в Builder, и чтениями из RegularImmutableList?

Ответ заключается в том, что все, что препятствовало доступу к объекту до того, как был вызван конструктор (что должно быть чем-то, в противном случае мы были бы полностью испорчены), продолжает препятствовать доступу к объекту до тех пор, пока конструктор не вернется. Конструктор фактически является атомарной операцией, потому что никакой другой поток не может попытаться получить доступ к объекту во время его работы. Как только конструктор возвращается, все, что поток, вызвавший конструктор, чтобы позволить другим потокам получить доступ к объекту, обязательно происходит после того, как конструктор возвращает, потому что "[e] ach действие в потоке происходит - перед каждым действием в этом потоке, которое происходит позже в порядке программы. "

И еще раз:

Если какой-то поток строит список и передает свою ссылку некоторым другим потокам без использования блокировок (например, через конечное или изменяемое поле), я не вижу, что гарантирует безопасность потока. Что мне не хватает?

Поток сначала строит список, а затем передает его ссылку. Построение списка "происходит - перед каждым действием в этом потоке, которое происходит позже в порядке программы" и, таким образом, происходит - до передачи ссылки. Таким образом, любой поток, который видит передачу ссылки, происходит после завершения построения списка.

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

Вы говорите о двух разных вещах здесь.

  1. Доступ к уже построенным RegularImmutableList И его array является потокобезопасным, потому что не будет никаких одновременных операций записи и чтения в этот массив. Только одновременное чтение.

  2. Проблема с потоками может возникнуть при передаче ее другому потоку. Но это не имеет ничего общего с RegularImmutableList но с тем, как другие темы видят ссылку на него. Допустим, один поток создает RegularImmutableList и передает свою ссылку в другой поток. Чтобы другой поток увидел, что ссылка была обновлена ​​и теперь указывает на новое созданное RegularImmutableList вам нужно будет использовать либо synchronization или же volatile,

РЕДАКТИРОВАТЬ:

Я думаю, что OP обеспокоен тем, как JMM гарантирует, что все, что было записано в array после его создания из одного строительного потока становится видимым для других потоков, после того как его ссылка передается им.

Это происходит путем использования или volatile или же synchronization, Когда, например, поток чтения назначает RegularImmutableList в переменную volatile JMM будет следить за тем, чтобы все записи в массив были перенесены в основную память, а когда другой поток читает из нее, JMM удостоверится, что он увидит все записанные записи.

Другие вопросы по тегам