用布尔双重检查成语

时间:2011-02-19 17:00:18

标签: java multithreading concurrency synchronization memory-model

采用以下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 发生在 {{1 }};如果列表不是reads,而是简单的ConcurrentLinkedQueueArrayList,即使它已被声明为LinkedListfinal也不需要发生在writes 之前。

那么,上面给出的代码是否没有数据竞争?

5 个答案:

答案 0 :(得分:5)

好的,让我们来看看Java语言规范。第17.4.5节defines happens-before如下:

  

a可以订购两个动作   发生在关系之前。如果一个   然后,行动发生在另一个之前   第一个是可见的和有序的   在第二个之前。如果我们有两个   动作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 发生在 {{1 }}

是无关紧要的。是的,如果线程从列表中读取一个值,我们可以得出结论,他也会在写入该值之前看到发生的任何事情。但如果它没有读取值呢?如果列表显示为空,该怎么办?即使它读取了一个值,也不意味着后续写入已经执行,因此列表可能看起来不完整。

答案 1 :(得分:4)

Wikipedia建议您使用volatile关键字。

答案 2 :(得分:3)

使用ConcurrentLinkedQueue并不能保证在这种情况下没有数据争用。 Its 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);进行重新排序。

所以,你有一个常规的复核成语,没有任何额外的保证,因此你需要按照Bozho的建议使用volatile

答案 3 :(得分:0)

您可以只检查someList.isEmpty()吗?

,而不是使用初始化标志

答案 4 :(得分:0)

首先,它是并发队列的错误使用。它适用于多个线程放入队列并轮询的情况。你想要的是一次初始化的东西,之后仍然是只读的。一个简单的列表就可以了。

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,那么“我可以将它改为任何List impl,而我只有一个地方可以改变”,他太天真了。 / p>