使用布尔值进行双重检查习语

发布于 2024-10-18 11:15:22 字数 1268 浏览 3 评论 0原文

采用以下 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 happen-before< /em> 读取;如果列表不是 ConcurrentLinkedQueue,而是一个简单的 ArrayListLinkedList,即使它已被声明为 final写入不需要发生在读取之前。

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

Take the following java code:

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...
    }
  }
}

The trick is that doSomeProcessing gets invoked only under certain conditions. Initializing the list is a very expensive procedure and it might not be needed at all.

I have read articles on why the double-check idiom is broken and I was a bit skeptic when I saw this code. However, the control variable in this example is a boolean, so a simple write instruction is needed, as far as I know.

Also, please notice that someList has been declared as final and keeps a reference to a concurrent list, whose writes happen-before reads; if instead of a ConcurrentLinkedQueue the list were a simple ArrayList or LinkedList, even though it has been declared as final, the writes don't require to happen-before the reads.

So, is the code given above free of data races?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

摘星┃星的人 2024-10-25 11:15:22

好的,让我们获取 Java 语言规范。第17.4.5节定义happens-before 如下:

两个动作可以通过
发生在关系之前。如果有一个
动作发生在另一个动作之前,然后
第一个可见并已订购
在第二个之前。如果我们有两个
动作 x 和 y,我们将 hb(x, y) 写入
表明 x 发生在 y 之前。

  • 如果 x 和 y 是相同的动作
    线程且 x 在程序中出现在 y 之前
    顺序,然后 hb(x, y)。
  • 有一个
    发生在 a 末尾的边之前
    对象的构造函数开始
    终结器(§12.6)的
    目的。
  • 如果一个动作 x
    与以下操作同步
    y,那么我们也有 hb(x, y)。
  • 如果
    hb(x, y) 和 hb(y, z),然后 hb(x, z)。

需要注意的是,存在
发生在之前的关系
两个动作之间不
必然意味着他们必须
按该顺序发生
执行。如果重新排序
产生的结果与
合法执行,不违法。

然后继续进行两个讨论:

更具体地说,如果两个操作共享“发生前”关系,则它们不一定必须按照该顺序发生在与它们不共享“发生前”关系的任何代码中。例如,一个线程中的写入与另一线程中的读取处于数据竞争中,这些读取可能看起来是无序发生的。

在您的实例中,线程检查

if (!initialized)

可能会在看到添加到 someList 的所有写入之前看到 initialized 的新值,从而使用部分填充的列表。

请注意你的论点

此外,请注意 someList 已被声明为 final 并保留对并发列表的引用,该并发列表的写入发生-之前 读取

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

Ok, let's get the Java Language Specification. Section 17.4.5 defines happens-before as follows:

Two actions can be ordered by a
happens-before relationship. If one
action happens-before another, then
the first is visible to and ordered
before the second. If we have two
actions x and y, we write hb(x, y) to
indicate that x happens-before y.

  • If x and y are actions of the same
    thread and x comes before y in program
    order, then hb(x, y).
  • There is a
    happens-before edge from the end of a
    constructor of an object to the start
    of a finalizer (§12.6) for that
    object.
  • If an action x
    synchronizes-with a following action
    y, then we also have hb(x, y).
  • If
    hb(x, y) and hb(y, z), then hb(x, z).

It should be noted that the presence
of a happens-before relationship
between two actions does not
necessarily imply that they have to
take place in that order in an
implementation. If the reordering
produces results consistent with a
legal execution, it is not illegal.

It then goes on two discuss:

More specifically, if two actions share a happens-before relationship, they do not necessarily have to appear to have happened in that order to any code with which they do not share a happens-before relationship. Writes in one thread that are in a data race with reads in another thread may, for example, appear to occur out of order to those reads.

In your instance, the thread checking

if (!initialized)

may see the new value for initialized before it sees all writes that added to someList and hence work with a partially filled list.

Note that your argument

Also, please notice that someList has been declared as final and keeps a reference to a concurrent list, whose writes happen-before reads

is irrelavant. Yes, if the thread read a value from the list, we could conclude that he also sees anything that happens-before that the write of that value. But what if it doesn't read a value? What if the list appears empty? And even if it read a value, it doesn't mean that subsequent writes have been performed, and hence the list may appear incomplete.

败给现实 2024-10-25 11:15:22

Wikipedia 建议您应该使用 volatile 关键字。

Wikipedia suggests that you should use the volatile keyword.

扛起拖把扫天下 2024-10-25 11:15:22

在这种情况下,使用 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 所建议的那样。

Use of ConcurrentLinkedQueue doesn't guarantee absence of data race in this case. Its javadoc says:

As with other concurrent collections, actions in a thread prior to placing an object into a ConcurrentLinkedQueue happen-before actions subsequent to the access or removal of that element from the ConcurrentLinkedQueue in another thread.

That is, it guarantees consistency in the following case:

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

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

So, in this case x = 42; can't be reordered with someList.add(...). However, this guarantee doesn't apply to the inverse situation:

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

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

In this case initialized = true; still can be reordered with someList.addAll(wsResult);.

So, you have a regular double-check idiom without any additional guarantees here, and therefore you need to use volatile, as suggested by Bozho.

倒数 2024-10-25 11:15:22

您可以只检查 someList.isEmpty() 而不是初始化标志吗?

Instead of having the initialized flag, can you just check someList.isEmpty()?

情深如许 2024-10-25 11:15:22

首先,是并发队列的错误使用。它适用于多个线程向队列中放入和轮询的情况。您想要的是初始化一次,然后保持只读状态的东西。一个简单的列表实现就可以了。

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,并且我只有一个地方可以更改”,他也是如此幼稚的。

First, it's the wrong use of the concurrent queue. It's intended for the situation where multiple threads are putting to and polling from a queue. What you want is something that's initialized once, then remains readonly afterwards. A simple list impl would do.

volatile ArrayList<String> list = null;

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

Suppose, for the sole purpose of brain exercise, we want to achieve thread safety through the concurrent queue:

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

Note, when declaring the variable, I used the full class type, instead of some interface. If someone argues that it should be declared interface List, so that "I can change it to any List impl, and I have only one place to change", he is too naive.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文