ConcurrentHashMap有十个提升性能的地方,你都知道吗?

本文深入探讨了ConcurrentHashMap在Java 7中的设计,揭示了其在高并发下提高系统性能的十个关键点,包括自旋锁、CAS操作、延迟写内存等技术。通过分析初始化、哈希计算、put方法、resize扩容和size方法,展示了Doug Lea如何通过精细的并发控制和优化策略实现线程安全的无锁读操作。
摘要由CSDN通过智能技术生成

一些题外话

如何在高并发下提高系统吞吐是所有后端开发者追求的目标,Java并发的开创者Doug Lea在Java 7 ConcurrentHashMap的设计中给出了一些参考答案,本文详细的总结了ConcurrentHashMap源码中影响并发性能的十个细节,有常见的自旋锁,CAS的使用,也有延迟写内存,volatile语义退化等不常见的技巧,希望对大家的开发设计有所帮助。

由于ConcurrentHashMap的内容比较多,而且Java 7和Java 8两个版本的实现相差比较大,如果采用我们上篇中对比的那种行文思路,在有限的篇幅中难免会遗漏一些细节,因此我决定采用两篇文章去详细阐述两个版本中ConcurrentHashMap技术细节,不过为了帮助读者体系化的理解,三篇文章(包含HashMap的那一篇)整体文章的结构将保持一致。

把书读薄

《阿里巴巴Java开发手册》的作者孤尽对ConcurrentHashMap的设计十分推崇,他说:“ConcurrentHashMap源码是学习Java代码开发规范的一个非常好的学习材料,我建议同学们可以时常去看一看,总会有新的收获的”,相信大家平常也能听到很多对于ConcurrentHashMap设计的溢美之词,在展开隐藏在ConcurrentHashMap所有小秘密之前,大家在大脑中首先要有这样的一幅图:

 

img

对于Java 7来说,这张图已经能完全把ConcurrentHashMap的架构说清楚了:

  1. ConcurrentHashMap是一个线程安全的Map实现,其读取不需要加锁,通过引入Segment,可以做到写入的时候加锁力度足够小
  2. 由于引入了Segment,ConcurrentHashMap在读取和写入的时候需要需要做两次哈希,但这两次哈希换来的是更细力粒度的锁,也就意味着可以支持更高的并发
  3. 每个桶数组中的key-value对仍然以链表的形式存放在桶中,这一点和HashMap是一致的。

把书读厚

关于Java 7的ConcurrentHashMap的整体架构,用上面三两句话就可以概括,这张图应该很快就可以在大家的大脑中留下印象,接下来我们通过几个问题来尝试吸引大家继续看下去,把书读厚:

  1. ConcurrentHashMap的哪些操作需要加锁?
  2. ConcurrentHashMap的无锁读是如何实现的?
  3. 在多线程的场景下调用size()方法获取ConcurrentHashMap的大小有什么挑战?ConcurrentHashMap是怎么解决的?
  4. 在有Segment存在的前提下,应该如何扩容的?

在上一篇文章中我们总结了HashMap中最重要的点有四个:初始化数据寻址-hash方法数据存储-put方法,扩容-resize方法,对于ConcurrentHashMap来说,这四个操作依然是最重要的,但由于其引入了更复杂的数据结构,因此在调用size()查看整个ConcurrentHashMap的数量大小的时候也有不小的挑战,我们也会重点看下Doug Lea在size()方法中的设计

初始化

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    // 保证ssize是大于concurrencyLevel的最小的2的整数次幂
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    // 寻址需要两次哈希,哈希的高位用于确定segment,低位用户确定桶数组中的元素
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

初始化方法中做了三件重要的事:

  1. 确定了segments的数组的大小ssize,ssize根据入参concurrencyLevel确定,取大于concurrencyLevel的最小的2的整数次幂
  2. 确定哈希寻址时的偏移量,这个偏移量在确定元素在segment数组中的位置时会用到
  3. 初始化segment数组中的第一个元素,元素类型为HashEntry的数组,这个数组的长度为initialCapacity / ssize,即初始化大小除以segment数组的大小,segment数组中的其他元素在后续put操作时参考第一个已初始化的实例初始化
static final class HashEntry<K,V> {
    final int hash; 
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next; 
 
    HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
        this.hash = hash;
        this.key = key;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值