1

为什么即使我们使用同步方法并因此获得对 Helper 对象的锁定,这段代码也不是线程安全的?

class ListHelper <E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());

    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !list.contains(x);
        if (absent)
            list.add(x);
        return absent;
    }
}
4

3 回答 3

11

因为列表在contains返回时被解锁,然后在add被调用时再次被锁定。其他东西可以在两者之间添加相同的元素。

如果您的意思是仅使用辅助对象中的列表,则应声明它private;如果你这样做,代码将是线程安全的,只要列表的所有操作都通过在帮助对象中同步的方法。还值得注意的是,只要是这种情况,您就不需要使用 a Collections.synchronizedList,因为您在自己的代码中提供了所有必要的同步。

或者,如果您想允许列表公开,您需要同步您对列表的访问,而不是在您的助手对象上。以下将是线程安全的:

class ListHelper <E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());

    public boolean putIfAbsent(E x) {
        synchronized (list) {
            boolean absent = !list.contains(x);
            if (absent)
               list.add(x);
            return absent;
        }
    }
}

不同之处在于它使用与列表的其他方法相同的锁,而不是不同的锁。

于 2013-07-08T18:14:36.890 回答
3

此代码不是线程安全的,仅因为 list 是公共的。

如果列表实例是私有的,并且没有在其他地方引用,则此代码是线程安全的。否则它不是线程安全的,因为多个线程可以同时操作列表。

如果列表没有在其他地方引用,则无需通过集合类将其声明为同步列表,只要所有列表操作都通过同步方法发生并且对该列表的引用永远不会返回给任何东西。

当您将方法标记为已同步时,调用该方法的所有线程都与定义该方法的对象实例同步。这就是为什么如果ListHelper内部列表实例未在其他地方引用,并且所有方法都是同步的,您的代码将是线程安全的。

于 2013-07-08T18:13:55.057 回答
1

线程安全的一个主要组成部分不仅仅是互斥。完全有可能完成对象状态的原子更新,即实现使对象处于有效状态且其不变量完好无损的状态转换,但如果对象的引用仍然发布到不可信或不完整的引用,则仍然使对象易受攻击调试的客户端。

在您发布的示例中:

public synchronized boolean putIfAbsent(E x) {
    boolean absent = !list.contains(x);
    if (absent)
        list.add(x);
    return absent;
}

正如 WM 所指出的,代码是线程安全的。但是我们不能保证x它本身以及它可能在哪里仍然有其他代码持有的引用。如果确实存在此类引用,则另一个线程可以修改列表中的相应元素,从而破坏您保护列表中对象不变量的努力。

如果您从不信任或不知道的客户端代码中接受此列表中的元素,一个好的做法是制作 x 的防御副本,然后将其添加到您的列表中。同样,如果您要将列表中的对象返回给其他客户端代码,则制作防御性副本并返回将有助于确保您的列表保持线程安全。

此外,列表应该完全封装在类中。通过将其公开,任何地方的客户端代码都可以自由访问元素,并使您无法保护列表中对象的状态。

于 2013-07-08T18:24:22.270 回答