在 Java 中减少同步块的范围意外损坏了我的 ArrayList,为什么会这样?

ran*_*321 2 java parallel-processing multithreading synchronization thread-safety

有点晚了,我有一个圣诞节特别给你。有一个圣诞老人班,里面有一个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;
}

Run Code Online (Sandbox Code Playgroud)

但是,使整个方法synchronized工作得很好。我真的不明白synchronized之前显示的较小尺寸的块的问题。从我的角度来看,它仍然应该确保礼物不会多次分配给孩子,并且礼物 ArrayList 上不应该有并发写入(和读取)。你能告诉我为什么我的假设是错误的吗?

dre*_*ash 5

发生这种情况是因为代码包含竞争条件。让我们用下面的例子来说明这种竞争条件。

想象一下,Thread 1

`if(gotPresent.containsKey(child) && !gotPresent.get(child))` 
Run Code Online (Sandbox Code Playgroud)

它评估为true. 当Thread 1进入synchronized块时,另一个线程(即, Thread 2)也读取

if(gotPresent.containsKey(child) && !gotPresent.get(child)) 
Run Code Online (Sandbox Code Playgroud)

之前Thread 1一直有时间做gotPresent.put(child, true);。因此,上述if也评估trueThread 2

Thread 1位于 内synchronized(this)并从礼物列表中删除礼物(即, presents.remove(present);)。现在size的的present名单0Thread 1退出synchronized块,而Thread 2刚刚进入它,并最终调用

int randomIndex = random.nextInt(presents.size()); 
Run Code Online (Sandbox Code Playgroud)

因为presents.size()会返回0random.nextInt实现如下:

  public int nextInt(int bound) {
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        ...
    }
Run Code Online (Sandbox Code Playgroud)

你得到了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;
    }
 }
Run Code Online (Sandbox Code Playgroud)

在前面提到的竞争条件示例中,Thread 2会在

if(gotPresent.containsKey(child) && !gotPresent.get(child))
Run Code Online (Sandbox Code Playgroud)

并且因为Thread 1,在退出同步块之前,会做

gotPresent.put(child, true);
Run Code Online (Sandbox Code Playgroud)

到时候Thread 2会进入synchronized块下面的语句

!gotPresent.get(child)
Run Code Online (Sandbox Code Playgroud)

将评估为false,因此Thread 2将立即退出而不调用 int randomIndex = random.nextInt(presents.size());大小列表0

由于您展示的方法是由多个线程并行执行的,因此您应该确保线程之间共享数据结构的互斥,即gotPresentpresents。这意味着,例如,该操作一样containsKeygetput应该在相同的synchronized块内执行。