在设计集合类时,有什么理由不私下实现锁定以使其线程安全?还是我应该把这个责任留给收藏品的消费者?
17 回答
是否有任何理由不私下实现锁定以使其线程安全?
这取决于。您的目标是编写一个可被多个线程访问的集合类吗?如果是这样,使其线程安全。如果没有,不要浪费你的时间。这种事情就是人们在谈论“过早优化”时所指的
解决您遇到的问题。不要试图解决你认为未来几年可能会有的未来问题,因为你看不到未来,你总是会错的。
注意:您仍然需要以可维护的方式编写代码,这样如果您确实需要加入并为集合添加锁定,这不会太难。我的观点是“不要实现你不需要也不会使用的功能”
对于 Java,您应该不同步以提高速度。如果需要,集合的消费者可以包装在同步包装器中。
线程安全集合可能具有欺骗性。Jared Par 发表了几篇关于线程安全集合的有趣文章:
问题是有几个级别的线程安全集合。我发现,当大多数人说线程安全集合时,他们真正的意思是“在从多个线程修改和访问时不会损坏的集合”</p>
...
但如果构建数据线程安全列表如此简单,微软为什么不在框架中添加这些标准集合呢?
回答: ThreadSafeList 是一个几乎无法使用的类,因为该设计会导致您走上错误代码的道路。
在您检查列表的常用方式之前,此设计中的缺陷并不明显。例如,下面的代码尝试从列表中抓取第一个元素(如果有的话)。
static int GetFirstOrDefault(ThreadSafeList<int> list) {
if (list.Count > 0) {
return list[0];
}
return 0; }
这段代码是一个经典的竞争条件。考虑列表中只有一个 > 元素的情况。如果另一个线程在 if 语句和 return 语句之间删除了该元素,return 语句将抛出异常,因为它试图访问列表中的无效索引。即使 ThreadSafeList 是数据线程安全的,也无法保证一个调用的返回值在对同一对象的下一次调用中的有效性
http://blogs.msdn.com/b/jaredpar/archive/2009/02/11/why-are-thread-safe-collections-so-hard.aspx
http://blogs.msdn.com/b/jaredpar/archive/2009/02/16/a-more-usable-thread-safe-collection.aspx
集合类需要尽可能快。因此,不要使用锁。
调用代码将知道集合类不知道锁最好的位置。在最坏的情况下,应用程序将不得不添加一个额外的锁,这意味着会发生两个锁,使其性能提高一倍。
我个人会把它留给消费者。它将使您的集合类更通用。
只需在您的文档中明确说明您没有使其成为线程安全并将其排除在外,或者,对于您的应用程序,如果您希望它是线程安全的,则使其成为线程安全的,并在您的文档中注明。唯一的规则是记录它。除此之外,为您制作课程,如果其他人想使用它,他们可以。
如果我正在寻找一个集合类并且我需要线程安全功能而您的类没有它们,我将立即跳到那里的下一个产品以查看它们提供的内容。你的收藏不会再引起我的注意了。
注意开头的“如果”。有些客户会想要它,有些不会,有些则不在乎。如果您要为消费者构建工具包,那么为什么不同时提供这两种工具呢?这样我可以选择使用哪一个,但如果我想要线程安全,你仍然需要我的注意,我不必自己写。
使集合线程安全是杀死 Java 的 Vector 和 Hashtable 类的原因。如前所述,客户端将其包装在线程安全的包装器中,或者在方法子集上同步数据访问,比每次访问类时都进行同步命中要容易得多。几乎没有人使用 Vector 或 Hashtable,如果他们使用了,他们会被嘲笑,因为它们的替代品(ArrayList 和 HashMap)要快得多。不幸的是,因为我(来自 C++ 背景)更喜欢“向量”名称(STL),但 ArrayList 仍然存在。
不使其线程安全的主要原因是性能。线程安全代码可能比非安全代码慢 100 倍,因此如果您的客户不想要该功能,那是相当大的浪费。
请注意,如果您尝试使任何类线程安全,则需要确定常见的使用场景。
例如,在集合的情况下,仅将所有属性和方法单独设置为线程安全对于消费者来说可能不够好,因为首先读取计数,然后循环或类似的,如果读后计数改变了。
基本上,将您的集合设计为线程安全的,并在您的类的两种方法中实现锁定:lock() 和 unlock()。在任何需要的地方打电话给他们,但不要让他们空着。然后子类你的集合实现 lock() 和 unlock() 方法。两个课程的价格。
不让你的集合线程安全的一个很好的理由是提高单线程性能。示例:ArrayList over Vector。将线程安全性推迟给调用者允许通过避免锁定来优化非同步用例。
使您的集合线程安全的一个非常好的理由是提高多线程性能。示例:基于 HashMap 的 ConcurrentHashMap。因为 CHM 内化了多线程关注点,它可以比外部同步更有效地对更大的并发访问进行条带锁定。
即使您知道您触摸的元素没有被其他任何人使用,这也会使您无法同时从多个线程访问集合。
一个示例是具有基于整数的索引访问器的集合。每个线程可能从它的 id 知道它可以访问哪些索引值,而不必担心脏读/写。
另一种可能对性能造成不必要影响的情况是,仅从集合中读取数据而不写入数据。
我同意把它留给消费者是正确的做法。If 为消费者提供了更大的灵活性,即是否同步 Collection 实例或同步不同的对象。例如,如果您有两个都需要更新的列表,则使用单个锁将它们放在单个同步块中可能是有意义的。
如果您创建一个集合类,请不要使其成为线程安全的。做正确(例如正确和快速)是相当困难的,而且当您做错时给您的消费者带来的问题(heisenbugs)也很难调试。
相反,实现一个 Collection API 并在需要时使用 Collections.synchronizedCollection(yourCollectionInstance) 来获得一个线程安全的实现。
只需在您的类 javadoc 中引用适当的 Collections.synchronizedXXX 方法即可;它将清楚地表明您在设计中考虑了线程安全,并确保消费者可以使用线程安全选项。
这是一个好的开始。
但是你会注意到你失去了集合的一大特点——枚举。您不能对枚举器进行线程安全,这实际上并不可行,除非您实现自己的枚举器,该枚举器将实例锁定返回给集合本身。我怀疑这会导致严重的瓶颈和潜在的死锁。
从 JDK 5 开始,如果您需要一个线程安全的集合,我首先会看看 java.util.concurrent 中已经实现的集合之一是否可以工作。正如Java Concurrency In Practice的作者(包括编写大部分类的人)指出的那样,正确实现这些是非常困难的,尤其是在性能很重要的情况下。
引用http://download.oracle.com/javase/6/docs/api/java/util/concurrent/package-summary.html
并发集合
除了队列之外,这个包还提供设计用于多线程上下文的集合实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问给定的集合时,ConcurrentHashMap 通常比同步的 HashMap 更可取,而 ConcurrentSkipListMap 通常比同步的 TreeMap 更可取。当预期的读取和遍历次数大大超过对列表的更新次数时,CopyOnWriteArrayList 比同步的 ArrayList 更可取。