使用布尔值进行双重检查习语
采用以下 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
,而是一个简单的 ArrayList
或 LinkedList
,即使它已被声明为 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
好的,让我们获取 Java 语言规范。第17.4.5节定义happens-before 如下:
然后继续进行两个讨论:
在您的实例中,线程检查
可能会在看到添加到
someList
的所有写入之前看到initialized
的新值,从而使用部分填充的列表。请注意你的论点
无关紧要。是的,如果线程从列表中读取一个值,我们可以得出结论,他也看到了在写入该值之前发生的任何事情。但如果它没有读取到值怎么办?如果列表显示为空怎么办?即使它读取了一个值,也不意味着已经执行了后续写入,因此列表可能看起来不完整。
Ok, let's get the Java Language Specification. Section 17.4.5 defines happens-before as follows:
It then goes on two discuss:
In your instance, the thread checking
may see the new value for
initialized
before it sees all writes that added tosomeList
and hence work with a partially filled list.Note that your argument
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.
Wikipedia 建议您应该使用
volatile
关键字。Wikipedia suggests that you should use the
volatile
keyword.在这种情况下,使用 ConcurrentLinkedQueue 并不能保证不存在数据争用。 它的javadoc说:
也就是说,它保证以下情况下的一致性:
因此,在这种情况下,
x = 42;
无法使用someList.add(...)
重新排序。但是,此保证不适用于相反的情况:在这种情况下,
initialized = true;
仍然可以使用someList.addAll(wsResult);
重新排序。因此,您有一个常规的双重检查习惯用法,没有任何额外的保证,因此您需要使用
volatile
,正如 Bozho 所建议的那样。Use of
ConcurrentLinkedQueue
doesn't guarantee absence of data race in this case. Its javadoc says:That is, it guarantees consistency in the following case:
So, in this case
x = 42;
can't be reordered withsomeList.add(...)
. However, this guarantee doesn't apply to the inverse situation:In this case
initialized = true;
still can be reordered withsomeList.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.您可以只检查 someList.isEmpty() 而不是初始化标志吗?
Instead of having the initialized flag, can you just check someList.isEmpty()?
首先,是并发队列的错误使用。它适用于多个线程向队列中放入和轮询的情况。您想要的是初始化一次,然后保持只读状态的东西。一个简单的列表实现就可以了。
假设,纯粹为了锻炼大脑,我们想通过并发队列来实现线程安全:
注意,在声明变量时,我使用了完整的类类型,而不是一些接口。如果有人认为应该将其声明为接口
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.
Suppose, for the sole purpose of brain exercise, we want to achieve thread safety through the concurrent queue:
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.