为什么即使使用同步方法,此代码也不是线程安全的?

时间:2013-07-08 18:12:30

标签: java thread-safety

为什么这个代码不是线程安全的,即使我们使用synchronized方法并因此获得对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;
    }
}

3 个答案:

答案 0 :(得分:11)

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

如果您只想使用辅助对象中的列表,则应将其声明为private;如果你这样做,代码将是线程安全的,只要列表的所有操作都通过在辅助对象中同步的方法。值得注意的是,只要是这种情况,您就不需要使用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;
        }
    }
}

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

答案 1 :(得分:3)

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

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

如果列表未在其他地方引用,则无需通过collections类将其声明为同步列表,只要所有列表操作都通过synchronized方法进行,并且对该列表的引用永远不会返回到任何内容。

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

答案 2 :(得分:1)

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

在您发布的示例中:

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

代码是线程安全的,如W.M.指出。但我们无法保证x本身以及它可能具有其他代码仍然存在的引用。如果确实存在这样的引用,则另一个线程可以修改列表中的相应元素,从而使您无法保护列表中对象的不变量。

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

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