Безопасность потоков Java в списке

У меня есть List, который должен использоваться как в потокобезопасном контексте, так и в потокобезопасном. Какой из них будет, невозможно заранее определить.

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

Collections.synchronizedList(...)

Но я не хочу его обертывать, если не вводит небезопасный контекст. F.e., потому что список огромен и интенсивно используется.

Я читал о Java, что его политика оптимизации строго о многопоточности - если вы не правильно синхронизируете свой код, не гарантируется, что он будет корректно выполняться в контексте между потоками - он может значительно реорганизовать код, обеспечивая согласованность в контексте только одного потока (см. http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.3). F.e.,

OP1; op2; op3;

может быть реорганизована в

op3; op2; OP1;

если он производит тот же результат (в контексте одного потока).

Теперь мне интересно, если я

  • заполнить мой список, прежде чем обернуть его synchronizedList,

  • затем заверните его,

  • затем используйте другой поток

- есть ли вероятность, что в другом потоке этот список будет заполнен только частично или вообще не заполнен? Может ли JVM отложить (1) до после (3)? Есть ли правильный и быстрый способ сделать (большой) список небезобезопасным, чтобы стать потокобезопасным?

Ответы

Ответ 1

Когда вы передаете свой список в другой поток потокобезопасными средствами (например, используя синхронизированный блок, переменную volatile или AtomicReference), гарантируется, что второй поток видит весь список в состоянии, в котором он был при передаче (или в любом более позднем состоянии, но не в более раннем состоянии).

Если вы не измените его впоследствии, вам также не понадобится ваш synchronizedList.


Изменить (после некоторых комментариев, чтобы сделать резервную копию моей заявки):

Я предполагаю следующее:

  • У нас есть изменчивая переменная list.

    volatile List<String> list = null;
    
  • Тема А:

    • создает список L и заполняет L элементами.
    • устанавливает list, чтобы указать на L (это означает запись L в list)
    • не вносит никаких изменений в L.

    Пример источника:

    public void threadA() {
       List<String> L = new ArrayList<String>();
       L.add("Hello");
       L.add("World");
       list = l;
    }
    
  • Тема B:

    • читает K из list
    • выполняет итерацию над K, печатая элементы.

    Пример источника:

    public void threadB() {
         List<String> K = list;
         for(String s : K) {
             System.out.println(s);
         }
    }
    
  • Все остальные темы не касаются списка.

Теперь мы имеем это:

  • Действия 1-A и 2-A в потоке A упорядочены по порядку программы, так что 1 до 2.
  • Действие 1-B и 2-B в потоке B упорядочивается порядок программы, так что 1 до 2.
  • Действие 2-A в потоке A и действие 1-B в потоке упорядочиваются порядок синхронизации, поэтому 2-A приходит до 1 -B, поскольку

    Запись в изменчивую переменную (§8.3.1.4) v синхронизируется со всеми последующими чтениями v любым потоком (где последующее определяется в соответствии с порядком синхронизации).

  • происходит до -order - это транзитивное закрытие программных заказов отдельных потоков и порядка синхронизации. Итак, мы имеем:

    1-A происходит до того, как произойдет 2-A - до того, как произойдет 1-B - до 2-B

    и, таким образом, 1-A происходит до 2-B.

  • Наконец,

    Если одно действие происходит - перед другим, то первое видно и упорядочивается до второго.

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


Еще одно редактирование (здесь, поскольку у меня больше свободы форматирования, чем в комментариях) о порядке программы Thread A. (Я также добавил пример кода выше).

Из JLS (раздел порядок программы):

Среди всех действий между потоками, выполняемых каждым потоком t, порядок программы of t - это полный порядок, который отражает порядок, в котором эти действия будут выполняется в соответствии с семантикой внутри потока t.

Итак, что такое внутрипоточная семантика потока A?

Некоторые пункты выше:

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

Остальная часть этой спецификации включает раздел 14.2 (Блоки):

Блок выполняется путем выполнения каждого объявления локальной переменной операторов и других операторов в порядке от первого до последнего (слева направо).

Итак, порядок программы действительно является порядком, в котором выражения/выражения приведены в исходном коде программы.

Таким образом, в нашем примере источник действия памяти создают новый ArrayList, добавляют "Hello", добавляют "World" и присваивают list (первые три состоят из большего количества подделок) действительно находятся в this порядок программы.

(В VM не нужно выполнять действия в этом порядке, но этот порядок программ по-прежнему вносит вклад в порядок дел и, следовательно, на видимость для других потоков.)

Ответ 2

Если вы заполните свой список, а затем оберните его в один поток, вы будете в безопасности.

Однако нужно иметь в виду несколько вещей:

  • Collections.synchronizedList() гарантирует вам только низкую безопасность потоков. Сложным операциям, таким как if ( !list.contains( elem ) ) list.add( elem );, по-прежнему будет нужен специальный код синхронизации.
  • Даже эта гарантия недействительна, если какой-либо поток может получить ссылку на исходный список. Убедитесь, что этого не происходит.
  • Сначала получите функциональность, затем вы можете начать беспокоиться о слишком медленной синхронизации. Я очень редко сталкивался с кодом, где скорость синхронизации Java была серьезным фактором.

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

Если x и y - действия одного и того же потока, а x - до y в программном порядке, то hb (x, y).

Вот почему заполнение списка, а затем его перенос в тот же поток является безопасным вариантом. Но что еще более важно:

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

Сообщение ясно: убедитесь, что ваша программа, выполненная в том порядке, в котором вы написали свой код, не содержит расов данных и не беспокойтесь о переупорядочении.

Ответ 3

Если траверсы случаются чаще, чем записи, я бы посмотрел на CopyOnWriteArrayList.

Нитевидный вариант ArrayList в которые все мутационные операции (добавить, набор и т.д.) реализуются создание новой копии массив.

Ответ 4

Посмотрите, как AtomicInteger (и подобные) реализованы, чтобы быть потокобезопасными и не синхронизироваться. Механизм не вводит синхронизацию, но если он необходим, он обрабатывает его изящно.