【问题标题】:Non-thread-safe Attempt to Implement Put-if-absent?非线程安全尝试实现 Put-if-absent?
【发布时间】:2014-04-15 14:10:57
【问题描述】:

Java并发实战第4章有一段代码sn-p

public 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;
    }
}

它说这对于使用不同的锁是线程安全的,putIfAbsent 相对于 List 上的其他操作而言不是原子的。 但是我认为防止多线程进入putIfAbsent的“同步”,如果有其他方法对List进行其他操作,关键字同步也应该作为方法属性。那么按照这种方式,它应该是线程安全的吗?什么情况下“不是原子的”?

【问题讨论】:

  • 供以后参考。始终将您锁定的对象设为私有,这样外部代码就不会在您不知情的情况下锁定它们。

标签: java multithreading concurrency


【解决方案1】:

putIfAbsent 相对于 List 上的其他操作而言不是原子的。但我认为“同步”防止多线程进入 putIfAbsent

这是真的,但不能保证线程可以通过其他方式访问listlist 字段是public(这总是一个坏主意),这意味着其他线程可以直接调用list 上的方法。要正确保护列表,您应该将其设为private,并将add(...) 和其他方法添加到您的ListHelper 中,这些方法也是synchronized,以完全控制所有 对同步列表的访问。

// we are synchronizing the list so no reason to use Collections.synchronizedList
private List<E> list = new ArrayList<E>();
...
public synchronized boolean add(E e) {
    return list.add(e);
}

如果列表是private,并且所有访问该列表的方法都已同步,那么您可以删除Collections.synchronizedList(...),因为您正在自己同步它。

如果还有其他方法对List进行其他操作,也应该将关键字synchronized作为方法属性。那么按照这种方式,它应该是线程安全的吗?

不确定我是否完全解析了这部分问题。但是,如果您将list 设为private 并添加其他方法来访问全部为synchronized 的列表,那么您是正确的。

什么情况下“不是原子的”?

putIfAbsent(...) 不是原子的,因为对同步列表有多次调用。如果多个线程正在列表上运行,则另一个线程可能在putIfAbsent(...) 调用list.contains(x) 和然后调用list.add(x) 之间调用list.add(...)Collections.synchronizedList(...) 保护列表免受多个线程的破坏,但是当对可能与来自其他线程的调用交错的列表方法进行多次调用时,它不能防止竞争条件。

【讨论】:

    【解决方案2】:

    任何修改列表的非同步方法都可能在 list.contains() 返回 false 之后、添加元素之前引入缺失元素。

    把它想象成两个线程:

    boolean absent = !list.contains(x); // Returns true
    -> list.add(theSameElementAsX);     // Another thread
    if(absent)   // absent is true, but the list has been modified!
       list.add(x);
    return absent;
    

    这可以通过以下简单的方法来完成:

    public void add(E e) {
        list.add(e);
    }
    

    如果方法是同步的,就没有问题,因为 add 方法在 putIfAbsent() 完全完成之前无法运行。

    适当的更正包括将 List 设为私有,并确保其上的复合操作正确同步(即在类或列表本身上)。

    【讨论】:

      【解决方案3】:

      线程安全是不可组合的!想象一个完全由“线程安全”类构建的程序。程序本身是“线程安全的”吗?不必要。这取决于程序对这些类的作用。

      synchronizedList 包装器使 List 的每个单独方法都“线程安全”。这意味着什么?这意味着在多线程环境中调用时,这些包装方法都不会破坏列表的内部结构

      这并不能保护任何给定程序使用列表的方式。在示例代码中,列表似乎被用作集合的实现:程序不允许同一个对象在列表中出现多次。但是,synchronizedList 包装器中没有任何东西会强制执行该特定保证,因为该保证与列表的内部结构无关。列表可以作为列表完全有效,但作为集合无效。

      这就是 putIfAbsent() 方法上额外同步的原因。

      【讨论】:

        【解决方案4】:

        Collections.synchronizedList() 创建一个集合,为它的每个 single 方法在私有互斥锁上添加同步。这个互斥量是 list this 用于示例中使用的单参数工厂,或者可以在使用双参数工厂时提供。这就是为什么我们需要一个外部锁来使后续的contains()add() 调用原子。

        如果列表直接可用,而不是通过ListHelper,则此代码被破坏,因为在这种情况下访问将由不同的锁保护。为防止这种情况,可以将list 设为私有以防止直接访问,并将所有必要的API 与ListHelper 中声明的同一互斥锁或ListHelper 本身的this 上声明的同一个互斥锁包装在一起。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-05-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多