Java 使用布尔式重复检查习惯用法
以以下java代码为例:Java 使用布尔式重复检查习惯用法,java,multithreading,concurrency,synchronization,memory-model,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() { /
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...
}
}
}
公共类SomeClass{
私有布尔初始化=false;
私人最终名单;
公共类(){
someList=新建ConcurrentLinkedQueue();
}
公共无效doSomeProcessing(){
//做些事情。。。
//检查列表是否已初始化
如果(!已初始化){
已同步(此){
如果(!已初始化){
//调用需要大量时间的Web服务
最终列表wsResult=invokeWebService();
addAll(wsResult);
初始化=真;
}
}
}
//列表已初始化
for(最终字符串s:someList){
//做更多的事情。。。
}
}
}
诀窍在于doSomeProcessing
仅在特定条件下被调用。初始化列表是一个非常昂贵的过程,可能根本不需要它
我读过关于为什么双重检查习惯用法被破坏的文章,当我看到这段代码时,我有点怀疑。然而,本例中的控制变量是布尔型的,所以据我所知,需要一条简单的写指令
另外,请注意,someList
已声明为final
,并保留对并发列表的引用,其写入发生在读取之前;如果列表不是ConcurrentLinkedQueue
而是简单的ArrayList
或LinkedList
,即使它已声明为final
,写入
不需要在读取
之前发生
那么,上面给出的代码没有数据竞争吗?建议您应该使用volatile
关键字。使用ConcurrentLinkedQueue
不能保证在这种情况下没有数据竞争。说:
与其他并发集合一样,在将对象放入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对代码>重新排序。添加(…)
。但是,本保证不适用于相反的情况:
// Thread 1
someList.addAll(wsResult);
initialized = true;
// Thread 2
if (!initialized) { ... }
for (final String s : someList) { ... }
在这种情况下,initialized=true代码>仍然可以使用someList.addAll(wsResult)重新排序代码>
因此,您有一个常规的双重检查习惯用法,这里没有任何额外的保证,因此您需要使用Bozho建议的volatile
。而不是使用初始化标志,您可以只检查someList.isEmpty()?好的,让我们看看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)
应当指出的是
一段感情之前发生的事
两个动作之间没有任何区别
必然意味着他们必须
以这种顺序在一个特定的时间内发生
实施如果重新排序
产生与目标一致的结果
合法执行,并不违法
然后进行两个讨论:
更具体地说,如果两个操作共享一个“发生在”关系,则它们不必按照该顺序出现在它们不共享“发生在”关系的任何代码中。例如,处于数据竞争中的一个线程中的写操作与另一个线程中的读操作可能会出现顺序错误
在您的实例中,线程检查
if (!initialized)
在看到添加到someList
的所有写入操作之前,可能会看到初始化的的新值,从而使用部分填充的列表
注意你的论点
另外,请注意,someList
已声明为final
,并保留对并发列表的引用,该并发列表的写入发生在读取之前
这是不相干的。是的,如果线程从列表中读取一个值,我们可以得出结论,在写入该值之前,他还看到了发生的任何事情。但是如果它没有读取值呢?如果列表显示为空怎么办?即使它读取了一个值,也不意味着已经执行了后续写入操作,因此列表可能不完整。首先,这是对并发队列的错误使用。它适用于多个线程从队列中放入和轮询的情况。您想要的是初始化一次,然后保持只读的东西。简单的列表impl就可以了
volatile ArrayList<String> list = null;
public void doSomeProcessing() {
// double checked locking on list
...
volatile数组列表=null;
公共无效doSomeProcessing(){
//双重检查列表上的锁定
...
假设,出于脑力锻炼的唯一目的,我们希望通过并发队列实现线程安全:
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
static final String END\u MARK=“一些永远不会成为有效结果的字符串”;
最终ConcurrentLinkedQueue队列=新建。。。
公共无效doSomeProcessing()
如果(!queue.contains(END_MARK))//检查费用高昂!
已同步(此)
如果(!queue.contains(结束标记))
结果=。。。
queue.addAll(结果);
//在包含之前发生(结束标记)=真
添加(结束标记);
//当我们在这里时,包含(完)