Что гарантирует безопасность потоков в Guava ImmutableList? - PullRequest
0 голосов
/ 08 ноября 2018

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

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

Но посмотрите, как 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), я не вижу, что гарантирует безопасность потока. Чего мне не хватает?

Edit:

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

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

Ответы [ 3 ]

0 голосов
/ 08 ноября 2018

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 .

0 голосов
/ 08 ноября 2018

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

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

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

Поток:

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

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

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

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

Переформулировать:

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

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

И еще раз:

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

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

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

0 голосов
/ 08 ноября 2018

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

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

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

EDIT:

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...