Дважды проверьте идиому с использованием логических значений - PullRequest
3 голосов
/ 19 февраля 2011

Возьмите следующий код Java:

public class SomeClass {
  private boolean initialized = false;
  private final List<String> someList; 

  public SomeClass() {
    someList = new ConcurrentLinkedQueue<String>();
  }

  public void doSomeProcessing() {
    // do some stuff...
    // check if the list has been initialized
    if (!initialized) {
      synchronized(this) {
        if (!initialized) {
          // invoke a webservice that takes a lot of time
          final List<String> wsResult = invokeWebService();
          someList.addAll(wsResult);
          initialized = true;
        }
      } 
    }
    // list is initialized        
    for (final String s : someList) {
      // do more stuff...
    }
  }
}

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

Я читал статьи о том, почему нарушена идиома двойной проверки, и я немного скептически отнесся к этому коду.Однако управляющая переменная в этом примере является логическим значением, поэтому, насколько я знаю, необходима простая инструкция записи.

Также обратите внимание, что someList был объявлен как final и сохраняетссылка на параллельный список, чей writes произойдет до reads;если вместо ConcurrentLinkedQueue список был простым ArrayList или LinkedList, даже если он был объявлен как final, writes не требуется произойти до reads.

Итак, приведенный выше код не содержит гонок данных?

Ответы [ 5 ]

5 голосов
/ 19 февраля 2011

Хорошо, давайте получим Спецификацию языка Java.Раздел 17.4.5 определяет случай до события следующим образом:

Два действия могут быть упорядочены с помощью отношения случай до.Если одно действие происходит раньше другого, то первое видно и упорядочено до второго.Если у нас есть два действия x и y, мы пишем hb (x, y), чтобы указать, что x происходит до y.

  • Если x и y являются действиями одного потока, а x предшествует yв программном порядке, то hb (x, y).
  • Для этого объекта существует крайний случай "до" от конца конструктора объекта до начала финализатора (§12.6).
  • Если действие x синхронизируется со следующим действием y, то у нас также есть hb (x, y).
  • Если hb (x, y) и hb (y, z), то hb (x, z).

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

Затем идет два обсуждения:

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

В вашем случае поток проверяет

if (!initialized)

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

Обратите внимание, что ваш аргумент

Также обратите внимание, что someList был объявлен как final и содержит ссылку на параллельный список, чей writes происходит до reads

не имеет отношения к делу.Да, если поток читает значение из списка, мы могли бы заключить, что он также видит все, что происходит - до того, как записывается это значение.Но что, если он не читает значение?Что если список окажется пустым?И даже если он читает значение, это не означает, что были выполнены последующие записи, и, следовательно, список может показаться неполным.

4 голосов
/ 19 февраля 2011

Википедия предлагает использовать ключевое слово volatile.

3 голосов
/ 19 февраля 2011

Использование ConcurrentLinkedQueue не гарантирует отсутствие гонки данных в этом случае. Его Javadoc говорит:

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

То есть он гарантирует согласованность в следующем случае:

// Thread 1
x = 42;
someList.add(someObject);

// Thread 2
if (someList.peek() == someObject) {
    System.out.println(x); // Guaranteed to be 42
}

Итак, в этом случае x = 42; не может быть переупорядочено с someList.add(...). Однако эта гарантия не распространяется на обратную ситуацию:

// Thread 1
someList.addAll(wsResult);
initialized = true;

// Thread 2
if (!initialized) { ... }
for (final String s : someList) { ... }

В этом случае initialized = true; все еще можно переупорядочить с помощью someList.addAll(wsResult);.

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

0 голосов
/ 20 февраля 2011

Во-первых, это неправильное использование параллельной очереди.Он предназначен для ситуации, когда несколько потоков помещаются в очередь и опрашивают ее.То, что вы хотите, это то, что инициализируется один раз, а затем остается только для чтения.Подойдет простой список impl.

volatile ArrayList<String> list = null;

public void doSomeProcessing() {
    // double checked locking on list
    ...

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

static final String END_MARK = "some string that can never be a valid result";

final ConcurrentLinkedQueue<String> queue = new ...

public void doSomeProcessing() 
    if(!queue.contains(END_MARK)) // expensive to check!
         synchronized(this)
            if(!queue.contains(END_MARK))
                  result = ...
                  queue.addAll(result);
                  // happens-before contains(END_MARK)==true
                  queue.add( END_MARK );

     //when we are here, contains(END_MARK)==true

     for(String s : queue)
         // remember to ignore the last one, the END_MARK

Обратите внимание, при объявлениипеременная, я использовал полный тип класса, а не какой-то интерфейс.Если кто-то утверждает, что должен быть объявлен интерфейс List, так что «Я могу изменить его на любой список Impl, и у меня есть только одно место для изменения» , он слишком наивен.

0 голосов
/ 19 февраля 2011

Вместо того, чтобы иметь инициализированный флаг, вы можете просто проверить someList.isEmpty ()?

...