【发布时间】:2011-02-19 17:00:18
【问题描述】:
取下面的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 reads;如果列表不是ConcurrentLinkedQueue,而是简单的ArrayList 或LinkedList,即使它已被声明为final,writes 也不需要发生之前 reads。
那么,上面给出的代码是否没有数据竞争?
【问题讨论】:
-
我更喜欢最简单的代码,避免同步块可以节省 2 微秒。但是,如果列表长度适中,使用 ConcurrentLinkedQueue 而不是 ArrayList 您可能会失去更多。更简单的代码通常也运行得更快。 ;)
-
仅供参考,在 Java5 之前的 Java 版本中,双重检查习语被破坏了。只要您将测试变量设置为 volatile,它就不再损坏。
-
When is assignment operation atomic in Java? 的可能重复项(无论是布尔值还是引用都无关紧要。)
-
@meriton:我看到了另一个问题。
helper = new Helper();语句可以分解为几个语句(分配内存,将这个新分配的地址分配给helper,然后实际运行构造函数代码),而initialized = true;语句是一个简单的写... i想想…… -
是的,分配初始化是原子的,就像分配助手一样。但是添加到列表(就像构造 Helper 实例一样)不是,并且允许 JVM 重新排序这些指令,即可以在列表(完全)附加之前分配初始化,就像可以在Helper 的构造函数已完成执行,因此另一个线程可能会在其完全初始化之前使用 someList(或 helper)。
标签: java multithreading concurrency synchronization memory-model