为什么 ConcurrentHashMap 不能为每个存储桶设置一个锁?

众所周知,Java的ConcurrentHashMap有许多内部锁,每个内部锁都保护着桶数组的某些区域。

一个问题是:为什么我们不能为每个桶创建一个锁

一个类似的问题已经被问到:Java ConcurrentHashMap中分区数量的增加的缺点?

根据答案,有几个原因:

  1. 同时运行的最大线程数受处理器内核数的限制。这是正确的吗?我们是否可以始终声明,如果我们有8核处理器,那么在ConcurrentHashMap中我们不需要超过8个锁定区域?

  2. 浪费了 L2 缓存。为什么?

  3. 浪费内存。看起来这是因为额外的锁创建。

还有什么原因吗?


答案 1

希望我在解释方面做得很好...此刻有点匆忙...

第一个问题的答案:

“为什么我们不能为每个桶创建一个锁?”

您可以为每个存储桶创建一个锁 - 它不一定是最好的行动方案。

您问题的答案:

“我们是否可以始终声明,如果我们有8核处理器,我们在ConcurrentHashMap中不需要超过8个锁定区域”

从技术上讲是“不”,尽管这取决于你所说的“需要”是什么意思。拥有多个与系统的最大并发性匹配或稍大的区域并不一定能防止争用,但在实践中,它运行良好。没有什么可以阻止两个线程同时尝试访问同一区域,即使有其他区域未锁定也是如此。

通过在 8 核处理器上拥有 8 个或更多区域,可以保证的是,可以同时访问所有区域而不会发生争用。如果您有 8 个内核(不是超线程),则最多可以同时执行 8 个操作。即便如此,理想的区域数可能多(例如,16 个)多于内核数,因为这将使争用的可能性降低,而且成本较低(只有 8 个额外的锁)。

拥有额外区域的好处最终会随着区域数量相对于最大并发性的增加而减少,这导致它们浪费空间(内存),如 JavaDoc 中所述。这是争用的可能性(假设一个区域有锁,另一个线程尝试访问它的概率是多少)和浪费空间之间的平衡。

还有其他几个因素会影响性能:ConcurrentHashMap

  • 锁定代码的执行时间 - 最好使锁定的代码段变小,以便它们快速完成并释放锁。释放锁的速度越快,解决争用的速度就越快。
  • 数据分布 - 在高并发性下,分布良好的数据往往表现更好。将所有数据聚类在单个区域中意味着您将始终遇到争用。
  • 数据访问模式 - 同时访问不同的数据区域将执行更好的操作,因为线程不会争用资源锁。如果一次只尝试访问一个区域,则拥有分布良好的数据并不重要。

无论有多少个区域,所有这三件事都会对性能产生积极或消极的影响,并可能使区域数量变得不那么相关。由于它们发挥了很大的作用,因此它们不太可能拥有更多的区域来帮助您。由于您只能同时执行这么多线程,因此拥有快速完成工作并释放其锁的线程是更好的焦点。

至于你关于缓存的问题:老实说我不确定,但我可以猜测。当您大量使用地图时,这些锁最终会在缓存上并占用空间,可能会碰出其他可能更有用的东西。缓存比主内存稀缺得多,缓存未命中会浪费大量时间。我认为这里的想法是普遍厌恶在缓存上放很多东西,这些东西并没有带来显着的好处。极端地说:如果缓存中充满了锁(以某种方式),并且每个数据调用都进入内存,那么性能就会受到影响。


答案 2

我们是否可以始终声明,如果我们有8核处理器,那么在ConcurrentHashMap中我们不需要超过8个锁定区域?

不,这是完全错误的。这取决于两个因素,线程数(并发性)和段冲突数。如果两个线程争用同一段,则一个线程可能会阻塞另一个线程。

虽然拥有内核的线程数量只能与拥有内核的线程数量一样多,但上述语句的最大错误是假设不在内核上运行的线程不能拥有锁。但是,拥有锁的线程仍然可以松动下一个线程的任务交换机上的CPU,然后在尝试获取相同的锁时被阻塞。

但是,将线程数调整为内核数并不罕见,特别是对于计算密集型任务。因此,a 的并发级别间接取决于典型设置中的内核数。ConcurrentHashMap


为每个存储桶设置一个锁定意味着为每个存储桶维护锁定状态和等待队列,这意味着需要相当多的资源。请记住,只有并发写入操作需要锁,而读取线程不需要锁。

但是,对于 Java 8 实现,此注意事项已过时。它对存储桶更新使用无等待算法,至少对于没有冲突的存储桶。这有点像每个桶都有一个锁,因为在不同桶上运行的线程不会相互干扰,但不会维护锁定状态和等待队列的开销。唯一需要注意的是给地图一个适当的初始大小。因此,如果指定了 ,则用作初始大小调整提示,但在其他情况下将被忽略。concurrencyLevel


推荐