缩小 Java 中同步块的范围意外破坏了我的 ArrayList,为什么会这样?
Reducing the scope of a synchronized block in Java unexpectedly corrupts my ArrayList, why is that the case?
有点晚了,我有一个圣诞节特别给你。有一个圣诞老人 class 带着 ArrayList
的礼物和一个 Map
来记录哪些 child 人已经收到了他们的礼物。 Children 建模为不断同时向圣诞老人索取礼物的线程。为简单起见,每个 child 只收到一个(随机)礼物。
这是圣诞老人 class 中的方法,偶尔会产生 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
对于 Thread 2
.
也计算为 true
Thread 1
在 synchronized(this)
内并从礼物列表中删除 present( 即 presents.remove(present);
)。现在 present
列表的 size
是 0
。 Thread 1
退出 synchronized
块,而 Thread 2
刚刚进入,并最终调用
int randomIndex = random.nextInt(presents.size());
由于presents.size()
会return0
,random.nextInt
实现如下:
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
...
}
你得到 IllegalArgumentException
异常。
However, making the whole method synchronized works just fine.
是的,因为
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
会立即退出,而不会使用大小为 0
.
的列表调用 int randomIndex = random.nextInt(presents.size());
由于您展示的方法是由多个线程并行执行的,因此您应该确保线程之间的共享数据结构互斥,即gotPresent
和presents
。这意味着,例如,像 containsKey
、get
和 put
这样的操作应该在同一个同步块中执行。
有点晚了,我有一个圣诞节特别给你。有一个圣诞老人 class 带着 ArrayList
的礼物和一个 Map
来记录哪些 child 人已经收到了他们的礼物。 Children 建模为不断同时向圣诞老人索取礼物的线程。为简单起见,每个 child 只收到一个(随机)礼物。
这是圣诞老人 class 中的方法,偶尔会产生 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
对于 Thread 2
.
true
Thread 1
在 synchronized(this)
内并从礼物列表中删除 present( 即 presents.remove(present);
)。现在 present
列表的 size
是 0
。 Thread 1
退出 synchronized
块,而 Thread 2
刚刚进入,并最终调用
int randomIndex = random.nextInt(presents.size());
由于presents.size()
会return0
,random.nextInt
实现如下:
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
...
}
你得到 IllegalArgumentException
异常。
However, making the whole method synchronized works just fine.
是的,因为
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
会立即退出,而不会使用大小为 0
.
int randomIndex = random.nextInt(presents.size());
由于您展示的方法是由多个线程并行执行的,因此您应该确保线程之间的共享数据结构互斥,即gotPresent
和presents
。这意味着,例如,像 containsKey
、get
和 put
这样的操作应该在同一个同步块中执行。