为什么Java的同步集合不使用读写锁?

18

在像HashtableVector这样的东西被弃用后,当集合同步包装器出现时,我认为同步会被更有效地处理。现在我查看了代码,惊讶地发现它只是用同步块包装了集合。

为什么像ReadWriteLock这样的东西没有包含在集合中,例如在Collections的SynchronizedMap中?是否存在某些效率考虑,使其不值得呢?


4
问设计师,这里只会得到猜测。我的猜测是,系列产品大约在1998年发布,而ReadLocks等则几年后发布。 - user207421
6个回答

14
读写锁是性能优化的一部分,意味着在特定情况下可以允许更大的并发性。必要条件是,它们应用于大部分时间被读取但不被修改的数据结构。
在其他情况下,它们比独占锁表现稍差,这是很自然的,因为它们具有更复杂的结构。
如果读写锁的锁通常保持在受保护资源上的一段适度长的时间,并且只进行了少量修改,则效率最高。
因此,读写锁是否比独占锁更好取决于使用情况。最终,您必须通过剖析来测量哪个锁执行得更好。
考虑到这一点,似乎更适合选择独占锁来处理 Collections.synchronizedMap 的一般用例,而不是具有主要读取器的特殊用例。 进一步链接

进入图像描述

[...] 然而,在写操作更普遍的配置中, 基于Sun 1.6.0_07 JVM的同步块版本比基于读写锁的版本快50% (基于Sun 1.5.0_15 JVM快14%)。

在只有1个读取器和1个写入器的低竞争情况下,性能差异较小, 并且三种类型的锁均可以产生最快的版本。

至少有一种机器/虚拟机配置(例如,请注意在使用Sun 1.5.0_15 JVM的2核机器上,ReentrantLocks是最快的)。


这并没有回答问题。他问的是为什么它们没有被用在SynchronizedMap中,例如。 - Gray
谢谢@platzhirsch。我必须说,然而,我并不完全相信ReadWriteLock比标准同步块更昂贵,而且优势似乎很大。如果您还有一分钟,能否详细说明成本差异?谢谢! - Miquel
2
@Miquel 我同意,它并不会更加昂贵,正如我所说,只是性能稍微差一些。但这只是我在理论上所知道的,虽然在处理这些话题时我遇到了一些有趣的基准测试,我试图在我的答案中添加一些以给出成本差异的具体数字。 - Konrad Reiche
2
@platzhirsch 非常感谢,你添加的数据真的让我大吃一惊。跟那些懂得支持自己观点的人交流真是一种愉悦! - Miquel
非常有趣的数字@platzhirsch。虽然我不喜欢只运行10秒钟的任何Java性能测试。根据代码大小,热交换编译器可能仍在运行。我会开始测试,运行一分钟,清除数字,然后再运行一个时间块。 - Gray
2
但是我最担心的是,这将成为那些试图自己编写锁机制的人的素材。对于99%的问题空间,“synchronized”都是推荐的,因为您不必主动清除锁。很多人都在进行过早的优化或者优化他们应用程序中错误的部分。但是话说回来,这也是需要记在心里的事情。 - Gray

7
我认为使用ReadWriteLock(如果您正在谈论的是这个)并不一定比使用synchronized关键字更快。这两种构造都会施加锁定并建立内存屏障和“happens before”限制。
您可能在谈论在Collections.synchronizedMap(...)和相关方法中做一些聪明的事情,其中读取方法被读锁定,写入方法被写锁定以提高性能。这可能对于java.util集合类可以很好地工作,但如果用户实现了Map,并且get()方法正在计数或类似的操作,则可能会导致同步问题-即一个“只读”的方法实际上对集合进行更新。是的,这样做是一个可怕的主意。 ConcurrentHashmap被编写成高性能,并直接使用volatile字段而不是synchronized块。这使代码与Collections.synchronizedMap(...)相比显着更复杂,但也更快。这就是为什么它被推荐用于高性能场景而不是Collections.synchronizedMap(new HashMap<...>())的原因。

哦,谢谢你的解释。那么你对“Hashtable”被弃用的整个想法持什么态度?为什么“ConcurrentHashmap”“遵守与Hashtable相同的功能规范”?或者为什么创建了ConcurrentHashmap而不是更新“Collections.synchronizedMap()”呢? - Miquel
1
HashTable是在Java 1.5并发类和集合写入之前编写的。它使用方法级别的synchronized语句,而不是使用内部互斥锁的synchronizedMap(...)。它比HashMap也更低效。这只是旧代码。 - Gray
正如我的回答所暗示的@Miquel,ConcurrentHashMap是从头开始编写的,旨在成为(顾名思义)高性能并发哈希映射。它比synchronizedMap(...)具有更高的性能,因为锁定更加细粒度。它也更加复杂,并且具有一些方法调用(例如ConcurrentMap方法),可以添加一些有用的并发功能。 - Gray

6

除了以下内容,大多数推理都已经解决。SynchronizedMap/Set/List以及Hashtable和Vector依赖于集合实例在同步状态下。因此,许多开发人员使用这种同步来确保原子性。例如。

List syncList = Collections.synchronizedList(new ArrayList());

//put if absent
synchronized(syncList){
   if(!syncList.contains(someObject)){
     syncList.add(someObject);
   }
}

这是线程安全的,并且对所有操作都是原子性的,因为synchronizedList将在自身上进行同步(即add、remove、get)。这就是Hashtable类没有改为支持锁分离类似于ConcurrentHashMap的主要原因。

因此,对于这些集合使用ReadWriteLock会失去原子操作的能力,除非您能够获取锁实例。


抱歉,我无法理解你的意思。如果你使用Hashtable,为什么不能按照代码片段中的示例使用synchronizedMap呢? - Miquel
1
你可以这样做,但我的回答解释了为什么在执行操作之前不能使用ReadWriteLock进行put if absent,除非你能够在操作之前获取写锁。我对于一个RWL支持的集合的假设是锁是隐藏的,你无法控制任何额外的原子操作。 - John Vint
现在我明白了,谢谢你的澄清! - Miquel

3

这是因为那些类在Java引入读/写锁之前就存在了(Java 5)。它们很少有用,因为它们的锁定方式已经被硬编码在其中,锁定粒度非常细。


1
如果他在谈论ReadWriteLock,那么根据javadocs,它是在1.5中引入的。 - Gray
谢谢大家。是的,它应该使用它们,但不,因为当时它们不可用。没关系。真遗憾!谢谢! - Miquel
线程协调是你自己应该做的事情,无论如何。同步集合能做到的最好的事情非常有限,仅限于一些实际应用场景。 - Marko Topolnik

0

这与效率无关。在 Collections.synchronizedMap 中使用 synchronized 是没有问题的。如果使用 ReadWriteLock 来实现 Map,我会称之为 Collections.LockedMap ;)

说正经的,Collections.synchronizedMap 是在 Lock 出现多年前编写的 API。它是一个发布后不能更改的 API。


1
同意。在@platzhirsch提出一些性能措施之前,以及Gray提到锁曝光问题之前,我并不清楚API需要改变,只是内部实现需要改变。 - Miquel

0

补充@JohnVint的答案,考虑迭代问题。 文档明确要求客户端同步:

It is imperative that the user manually synchronize on the returned list when iterating over it:

List list = Collections.synchronizedList(new ArrayList());
    ...
synchronized (list) {
    Iterator i = list.iterator(); // Must be in synchronized block
    while (i.hasNext())
        foo(i.next());
}

Failure to follow this advice may result in non-deterministic behavior.

这仅在客户端可以共享返回的映射上的内在锁时才起作用。如果它在内部使用读/写锁,则返回的接口必须支持访问至少读取锁以进行安全迭代的某种方式。这将为可疑的好处(如其他人所解释的那样)使API变得复杂。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接