如何解决在 Java 中减少同步块的范围意外损坏了我的 ArrayList,为什么会这样?
有点晚了,我为你准备了圣诞特别节目。有一个圣诞老人班级,有 ArrayList
个礼物和一个 Map
来跟踪哪些孩子已经收到了礼物。孩子们模仿成线程不断地同时向圣诞老人索要礼物。为简单起见,每个孩子都会收到一份(随机)礼物。
这是 Santa 类中的方法偶尔会产生 IllegalArgumentException
,因为 presents.size()
是负数。
public Present givePresent(Child child) {
if(gotPresent.containsKey(child) && !gotPresent.get(child)) {
synchronized(this) {
gotPresent.put(child,true);
Random random = new Random();
int randomIndex = random.nextInt(presents.size());
Present present = presents.get(randomIndex);
presents.remove(present);
return present;
}
}
return null;
}
然而,使整个方法 synchronized
工作得很好。我不太明白前面显示的较小尺寸的 synchronized
块的问题。从我的角度来看,它仍然应该确保礼物不会多次分配给孩子,并且礼物 ArrayList 上不应该有并发写入(和读取)。你能告诉我为什么我的假设是错误的吗?
解决方法
发生这种情况是因为代码包含一个竞争条件。让我们用下面的例子来说明这种竞争条件。
想象一下 Thread 1
读取
`if(gotPresent.containsKey(child) && !gotPresent.get(child))`
并且它的计算结果为 true
。当 Thread 1
进入 synchronized
块时,另一个线程(即 Thread 2
)也读取
if(gotPresent.containsKey(child) && !gotPresent.get(child))
在Thread 1
有时间做gotPresent.put(child,true);
之前。因此,上述 if
对于 true
的计算结果也为 Thread 2
。
Thread 1
位于 synchronized(this)
内并从礼物列表中删除 present(即 presents.remove(present);
)。现在 size
列表的 present
是 0
。 Thread 1
退出 synchronized
块,而 Thread 2
只是进入它,并最终调用
int randomIndex = random.nextInt(presents.size());
由于presents.size()
会返回0
,random.nextInt
的实现如下:
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
...
}
您会遇到 IllegalArgumentException
异常。
但是,使整个方法同步就可以了。
是的,因为与
synchronized(this) {
if(gotPresent.containsKey(child) && !gotPresent.get(child)) {
gotPresent.put(child,true);
Random random = new Random();
int randomIndex = random.nextInt(presents.size());
Present present = presents.get(randomIndex);
presents.remove(present);
return present;
}
}
在前面提到的竞争条件示例中,Thread 2
会在
if(gotPresent.containsKey(child) && !gotPresent.get(child))
并且因为Thread 1
,在退出同步块之前,会完成
gotPresent.put(child,true);
到 Thread 2
将进入 synchronized
块时,以下语句
!gotPresent.get(child)
将评估为 false
,因此 Thread 2
将立即退出,而无需使用大小为 int randomIndex = random.nextInt(presents.size());
的列表调用 0
。
由于您展示的方法是由多个线程并行执行的,因此您应该确保线程之间共享数据结构(即 gotPresent
和 presents
)的互斥。例如,这意味着 containsKey
、get
和 put
之类的操作应该在同一个同步块中执行。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。