juca:Striped64

Posted redreampt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了juca:Striped64相关的知识,希望对你有一定的参考价值。

Striped64类

Striped64是java1.8 juca中新增的多个计数器类的基础类。它的基本思想其实与并发数据结构的发展息息相关:

  • 最原始的并发数据结构使用粗粒度的阻塞锁。如HashTable,直接将并行转换为串行,性能很差。
  • 然后的想法是改进锁的粒度,仍然使用阻塞锁,但对加锁范围进行限定。如ConcurrentHashMap(1.6,1.7),使用了与CPU个数有关的桶,每个桶单独一个锁,但线程增多,锁争用的情况仍然明显。
  • 再然后是抛弃阻塞锁,因为阻塞锁的睡眠唤醒机制需要陷入内核,直接使用底层的硬件机制如CAS,进行无锁同步。如AtomicXXX系列类。
  • 现在是避免线程争用,无论是锁还是CAS,如本文要探讨的Striped64,以及ConcurrentHashMap(1.8)。让每个线程使用不同的单元,从而避免争用导致的性能下降。

当然具体情况还要具体分析,阻塞锁,无锁(CAS,自旋锁),避免线程争用(单元个数)都需要在考虑实际的情况下使用,比如:

  • 阻塞锁,阻塞锁实在自旋锁的基础上发明的,是为了解决多个等待线程自旋所造成的CPU浪费,而引入了等待线程队列、wait()和awake()系统原语。如果临界区内的工作任务较重,就比较适合使用阻塞锁而非自选锁。比如ConcurrentHashMap。
  • 无锁,采用CPU为实现锁提供的底层机制,是对锁使用的一种本质回归。如果临界区内的工作任务较轻,如只是加减数,就比较适合使用无锁。比如AtomicXXX。
  • 线程争用。ConcurrentHashMap(1.8)因为其结构性质,天然适合对每个插槽单独进行同步处理,因此它的同步基本单元可以近乎无限增长(当然存在上限,但一般不用考虑),而与CPU个数大小没有太大关系。Striped64却没必要这么做,过度的分散并不会明显的增长性能,反而影响汇总操作。

设计思想

具体的设计思想可以参考其注释:

     这个类维护一个懒惰初始化的原子更新变量的表,外加一个额外的“base”字段。table的大小是2的幂。
     索引使用掩码下的每个线程的hash code。这个类中几乎所有的声明都是包私有的,由子类直接访问。

     表项是Cell类,AtomicLong填充(通过@sun.misc.Contended)的一个变体,以减少缓存争用。
     对于大多数原子类来说,填充是多余的,因为它们通常不规则地分散在内存中,因此不会相互干扰。
     但是驻留在数组中的原子对象将倾向于彼此相邻放置,因此在没有这种预防措施的情况下,最常见的
     情况是共享缓存行(具有巨大的负面性能影响)。

     部分原因是因为Cells相对较大,所以我们避免创建Cells,直到需要它们。当没有争用时,
     将对base字段进行所有更新。第一次争用时(base更新时CAS失败),该表被初始化为大小2。
     在进一步竞争时,表的大小加倍,直到达到大于或等于CPU数量的2的最近幂。表插槽保持
     为空(null),直到需要为止。

     单个自旋锁(“cellsBusy”)用于初始化表,调整其大小,以及用新的Cells填充插槽。
     不需要阻塞锁;当锁不可用时,线程会尝试其他插槽(或base)。在这些重试过程中,竞争加剧,
     局部性降低,但这仍然优于替代方案。

     通过ThreadLocalRandom维护的Thread.probe字段用作每个线程的哈希代码。我们让它们保持未初始化(为零)
     (如果它们以这种方式出现),直到它们在插槽0竞争。然后将它们初始化为通常不会与其他值冲突的值。
     执行更新操作时,竞争和/或表冲突由失败的情况指示。发生冲突时,如果表的大小小于容量,
     那么它的大小将加倍,除非其他线程持有锁。如果散列槽为空,并且锁可用,则会创建一个新的Cells。
     否则,如果插槽存在,将尝试CAS。重试通过“双散列”进行,使用二级散列(Marsaglia XorShift)
     来尝试找到空闲插槽。

     表的大小是有上限的,因为当线程多于CPU时,假设每个线程都绑定到一个CPU,就会有一个完美的
     散列函数将线程映射到插槽,从而消除冲突。当我们达到容量时,我们通过随机改变冲突线程的哈希代码
     来搜索这个映射。因为搜索是随机的,冲突只有通过CAS失败才知道,收敛可能会很慢,而且因为线程通常
     不会永远绑定到CPU,所以根本不会发生。然而,尽管有这些限制,在这些情况下观察到的竞争率通常很低。

     当一个Cell的散列到它的线程一旦终止时,以及在表大小加倍导致没有线程在扩展掩码下散列到它的情况下,
     该Cell可能会变成未使用。我们不会试图检测或移除这些Cell,因为考虑对于长期运行的情况,观察到的
     竞争级别会再次出现,因此最终会再次需要这些Cell。短时间的未使用,并不重要。

以上是关于juca:Striped64的主要内容,如果未能解决你的问题,请参考以下文章

Juc16_LongAdder引入原理Striped64分散热点思想深度解析LongAdder源码和AtomicLong区别

juca:LongAdder

Java中的并发计数器LongAdder

Java中的并发计数器LongAdder

jdk源码解析--LongAdder类

c_cpp 快速代码片段,用于在统计(阻止)/ dev / rdsk中的设备时验证fstat64和stat64的行为。