ConcurrentHashMap的底层实现与深度分析

简介: 【11月更文挑战第15天】在Java并发编程中,ConcurrentHashMap是一个非常重要的数据结构,它提供了一种线程安全的哈希表实现。随着Java版本的迭代,ConcurrentHashMap的实现也在不断优化,以更好地支持高并发场景。

一、背景介绍

在Java并发编程中,ConcurrentHashMap是一个非常重要的数据结构,它提供了一种线程安全的哈希表实现。随着Java版本的迭代,ConcurrentHashMap的实现也在不断优化,以更好地支持高并发场景。本文将深入探讨ConcurrentHashMap的底层存储结构、红黑树转换时机、核心属性sizeCtl、散列算法、计数器的安全机制以及size方法的实现策略。通过对这些功能点的详细分析,我们将揭示ConcurrentHashMap如何在高并发环境下保持高效性和线程安全性。

二、底层存储结构

2.1 存储结构概述

在Java 8及以后的版本中,ConcurrentHashMap采用了数组、链表和红黑树的组合结构。默认情况下,ConcurrentHashMap会初始化一个长度为16的数组,数组的每个元素都是一个链表或红黑树的头节点。这种设计旨在平衡查询效率和空间占用。

2.2 数组

数组是ConcurrentHashMap存储哈希表的基本结构。通过哈希函数,键被映射到数组的一个索引上。如果多个键的哈希值相同(即发生了哈希冲突),它们将被存储在同一个链表或红黑树上。

2.3 链表

链表用于解决哈希冲突。当多个元素哈希值相同时,它们会被存储在同一个链表上。链表的插入和删除操作的时间复杂度为O(n),其中n为链表的长度。

2.4 红黑树

红黑树是一种自平衡的二叉搜索树,用于在链表长度过长时提高查询效率。当链表长度超过8且数组长度大于64时,链表会转换成红黑树。红黑树的插入、删除和查找操作的时间复杂度为O(logn),其中n为树中节点的数量。

2.5 底层代码实现

以下是ConcurrentHashMap初始化数组和Node节点的部分代码:

java复制代码
// 初始化数组
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        elseif (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
break;
        }
    }
return tab;
}
// Node节点定义
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
    Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
    }
// 省略其他方法...
}

三、红黑树转换时机

3.1 转换时机概述

当链表长度超过8且数组长度大于64时,链表会转换成红黑树。这一转换旨在优化查询性能,避免因链表过长导致的查询慢问题。

3.2 转换条件

  • 链表长度大于等于8。
  • 数组长度大于等于64。

如果链表长度超过8但数组长度小于64,则先进行数组扩容操作(数组长度变为原来的二倍),然后再考虑是否将链表转换为红黑树。

3.3 转换时机代码实现

以下是链表转红黑树的部分代码:

java复制代码
// treeifyBin方法尝试将链表转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1); // 先扩容
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
if ((p.prev = tl) == null)
                            hd = p;
else
                            tl.next = p;
                        tl = p;
                    }
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

3.4 转换时机优化

链表转红黑树的优化主要体现在以下两个方面:

  • 空间和时间平衡:红黑树节点占用空间是普通节点的两倍,因此只有在链表长度足够长时才进行转换,以避免不必要的空间浪费。
  • 哈希算法优化:如果发现ConcurrentHashMap内部出现了红黑树结构,可能意味着哈希算法需要优化,以减少哈希冲突。

四、核心属性sizeCtl

4.1 sizeCtl概述

sizeCtl是ConcurrentHashMap中的一个核心属性,用于控制初始化和扩容操作。它在扩容过程中表示当前扩容的状态,并在初始化时表示默认的初始容量。

4.2 sizeCtl的值域

sizeCtl的值域分为以下几种情况:

  • sizeCtl > 0:表示容器容量。
  • sizeCtl = 0:表示默认初始值。
  • sizeCtl = -1:表示table正在初始化。
  • sizeCtl < -1:表示容器正在扩容;高16位存储SizeStamp(扩容版本号),低16位表示有n-1个线程正在参与扩容。

4.3 sizeCtl在初始化中的作用

在初始化过程中,sizeCtl的值用于控制并发初始化的线程安全。当多个线程尝试同时初始化数组时,只有一个线程能够成功将sizeCtl的值从默认值修改为-1,并获得初始化数组的权限。其他线程则通过自旋等待初始化完成。

4.4 sizeCtl在扩容中的作用

在扩容过程中,sizeCtl的值用于表示当前扩容的状态和进度。扩容操作会创建一个新的数组,并将旧数组中的元素迁移到新数组中。sizeCtl的高16位存储扩容版本号,用于区分连续的多次扩容;低16位表示当前参与扩容的线程数量。

4.5 sizeCtl相关代码实现

以下是sizeCtl在初始化和扩容过程中的部分代码实现:

java复制代码
// 初始化数组时修改sizeCtl的值
private final Node<K,V>[] initTable() {
// ... 省略部分代码 ...
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// ... 省略部分代码 ...
            sizeCtl = n - (n >>> 2); // 设置下次扩容的阈值
        } finally {
// ... 省略部分代码 ...
        }
    }
// ... 省略部分代码 ...
}
// 扩容时修改sizeCtl的值
private final void addCount(long x, int check) {
// ... 省略部分代码 ...
if (check >= 0) {
// ... 省略部分代码 ...
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
// ... 省略并发控制代码 ...
            } else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                           (rs << RESIZE_STAMP_SHIFT) + 2)) {
// 进行扩容
                transfer(tab, null);
// ... 省略部分代码 ...
            }
        }
    }
// ... 省略部分代码 ...
}

五、散列算法

5.1 散列算法概述

散列算法是一种将任意长度的消息压缩到一个固定长度的输出的算法。在ConcurrentHashMap中,散列算法用于将键映射到一个固定的桶中。

5.2 散列算法步骤

ConcurrentHashMap使用的散列算法主要包括以下步骤:

  1. 计算哈希值:将键的hashCode()值通过位运算的方式得到一个哈希值。
  2. 异或运算:将哈希值与一个常数进行异或运算,增加哈希值的随机性。
  3. 取模运算:将异或运算后的哈希值与桶的数量进行取模运算,得到一个桶的下标。

5.3 散列算法优化

ConcurrentHashMap中的散列算法通过以下方式进行了优化:

  • 高位和低位哈希值结合:通过位运算将键的哈希值分为高位和低位,并结合高位和低位哈希值计算出最终的哈希索引,以提高哈希分布的均匀性。
  • 减少哈希冲突:通过优化哈希算法和扩容机制,减少哈希冲突的发生,提高查询性能。

5.4 散列算法代码实现

以下是ConcurrentHashMap中散列算法的部分代码实现:

java复制代码
// 计算哈希值的spread方法
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
// 基于key计算索引位置
int i = (n - 1) & spread(key.hashCode());

六、计数器的安全机制

6.1 计数器概述

ConcurrentHashMap使用计数器来跟踪元素的数量。在并发环境下,计数器的更新操作需要保证线程安全。

6.2 计数器的安全机制

为了保证计数器的线程安全,ConcurrentHashMap采用了以下安全机制:

  • CAS操作:在更新计数器时,使用CAS(Compare-And-Swap)操作来确保数据的一致性和完整性。
  • LongAdder:在Java 8及以后的版本中,ConcurrentHashMap没有直接使用AtomicInteger等原子类来实现计数器,而是基于LongAdder的原理进行了优化。LongAdder通过将计数操作分散到多个Cell中,减少了CAS操作的竞争,提高了并发性能。

6.3 计数器的代码实现

以下是ConcurrentHashMap中计数器更新和读取的部分代码实现:

java复制代码
// 更新计数器
private final void addCount(long x, int check) {
// ... 省略部分代码 ...
if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// ... 省略并发控制代码 ...
    }
}
// 读取计数器
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
                sum += a.value;
        }
    }
return sum;
}

七、size实现策略

7.1 size方法概述

ConcurrentHashMap的size()方法用于返回当前映射中键值对的数量。由于ConcurrentHashMap设计为线程安全且高效,其size()方法在实现上需要考虑并发环境下的性能和一致性。

7.2 size方法的实现步骤

ConcurrentHashMap中size()方法的实现通常包含以下步骤:

  1. 遍历所有段:由于ConcurrentHashMap采用分段结构,每个段都有自己的计数器来跟踪该段中元素的数量。因此,需要遍历所有段来获取每个段的大小。
  2. 累加段大小:将每个段的大小累加起来以获得总大小。
  3. 考虑并发情况:由于在获取大小的过程中可能有其他线程正在进行添加或删除操作,因此返回值可能不是完全准确的。但ConcurrentHashMap尽量确保返回值的准确性,在短时间内保持一致性。

7.3 size方法的代码实现

以下是ConcurrentHashMap中size()方法的简化版代码实现:

java复制代码
public int size() {
long sum = 0;
for (Segment<K,V> segment : segments) {
        sum += segment.count;
    }
return (int)sum;
}

需要注意的是,上述代码是简化版实现,实际实现可能更加复杂,并涉及到并发控制等细节。

7.4 size方法的优化

为了提高size()方法的性能,ConcurrentHashMap采用了以下优化措施:

  • 分段计数器:通过分段计数器减少锁竞争,提高并发性能。
  • 弱一致性:在并发环境下,允许size()方法返回一个近似值,以提高性能。

八、总结与展望

8.1 总结

本文深入探讨了ConcurrentHashMap的底层存储结构、红黑树转换时机、核心属性sizeCtl、散列算法、计数器的安全机制以及size方法的实现策略。通过对这些功能点的详细分析,我们揭示了ConcurrentHashMap如何在高并发环境下保持高效性和线程安全性。

8.2 展望

随着Java版本的迭代和硬件技术的发展,ConcurrentHashMap的实现也将不断优化。未来,我们可以期待ConcurrentHashMap在以下几个方面取得进展:

  • 更高效的并发控制机制:通过引入更先进的并发控制机制(如无锁数据结构、乐观锁等),进一步提高ConcurrentHashMap的并发性能。
  • 更智能的扩容策略:通过引入更智能的扩容策略(如动态调整扩容阈值、根据负载情况自动扩容等),减少扩容操作对性能的影响。
  • 更灵活的配置选项:提供更多的配置选项,允许开发者根据实际应用场景调整ConcurrentHashMap的行为(如哈希算法、负载因子等),以满足不同场景的需求。

通过持续的优化和创新,ConcurrentHashMap将继续在Java并发编程中发挥重要作用。

相关文章
|
3月前
|
SQL Java 数据库连接
MyBatis 分页机制详解:从 RowBounds 到物理分页实践
MyBatis分页策略解析:逻辑分页(RowBounds)将全量数据加载至内存,仅适用于小数据量;物理分页通过SQL层面限制返回数据,性能更优。推荐使用PageHelper插件,自动适配数据库方言,一行代码实现高效分页,避免OOM风险,提升系统稳定性。
|
机器学习/深度学习 编解码 算法
高真实感3D高斯数字化身
本次分享介绍了3D高速扩建高新作为一种新的可微渲染技术,特别是高斯泼溅技术在数字化身3D领域的应用。该技术通过高斯点云扩展传统3D点云属性,实现高真实感、实时交互渲染,优化3D重建与多视点图像生成。文中还探讨了数字化身的构建与应用,包括全身和人头模型的创建,并展示了其在不同环境光照下的效果。最后,提出了未来研究方向,如更灵活的编辑和视频生成大模型的融合,以提升数字人的可控性和真实感。
|
缓存 Java 数据库连接
Mybatis一级缓存、二级缓存详讲
本文介绍了MyBatis中的查询缓存机制,包括一级缓存和二级缓存。一级缓存基于同一个SqlSession对象,重复查询相同数据时可直接从缓存中获取,减少数据库访问。执行`commit`操作会清空SqlSession缓存。二级缓存作用于同一namespace下的Mapper对象,支持数据共享,需手动开启并实现序列化接口。二级缓存通过将数据存储到硬盘文件中实现持久化,为优化性能,通常在关闭Session时批量写入缓存。文章还说明了缓存的使用场景及注意事项。
661 7
Mybatis一级缓存、二级缓存详讲
|
缓存 NoSQL Java
高并发场景秒杀抢购超卖Bug实战重现
在电商平台的秒杀活动中,高并发场景下的抢购超卖Bug是一个常见且棘手的问题。一旦处理不当,不仅会引发用户投诉,还会对商家的信誉和利益造成严重损害。本文将详细介绍秒杀抢购超卖Bug的背景历史、业务场景、底层原理以及Java代码实现,旨在帮助开发者更好地理解和解决这一问题。
484 12
|
11月前
|
传感器 人工智能 算法
智能硬件创新:产品技术双驱,不玩 AI 硬凑
在智能硬件快速发展的今天,仅靠“AI+硬件”的简单叠加难以实现真正创新。产品创新需以用户需求为核心,从设计、功能到体验全方位优化;技术创新则通过新材料、新工艺和新算法提升性能、降低成本。两者深度融合、双轮驱动,方能打造卓越的智能硬件。例如苹果iPhone与大疆无人机,均凭借产品和技术的协同突破成为行业标杆。企业应避免常见“坑”,如用户体验不佳、技术难度高和成本控制难,以真创新引领市场。联系法思诺,探索更多创新可能!
368 0
|
安全 Java 编译器
HashMap, HashTable, ConcurrentHashMap 之间的区别
HashMap, HashTable, ConcurrentHashMap 之间的区别
425 0
|
安全 Java
ConcurrentHashMap扩容的详细介绍以及多线程测试(基于JDK1.8)
ConcurrentHashMap扩容的详细介绍以及多线程测试(基于JDK1.8) ConcurrentHashMap是Java中线程安全的哈希表实现。它使用了锁分段技术,将哈希表分成多个Segment(默认为16),每个Segment都是一个独立的哈希表,每个Segment内部维护一个ReentrantLock锁,对于不同的Segment,它们可以被并发访问。当进行put、get等操作时,只需要获取对应Segment的锁即可,大大提高了并发访问的效率。
577 0
|
边缘计算 物联网 vr&ar
一文带你彻底了解Wi-Fi 7
【9月更文挑战第1天】
1858 0
一文带你彻底了解Wi-Fi 7
|
安全 Java
【Java集合类面试十六】、HashMap与ConcurrentHashMap有什么区别?
HashMap是非线程安全的,而ConcurrentHashMap通过减少锁粒度来提高并发性能,检索操作无需锁,从而提供更好的线程安全性和性能。
|
存储 运维 Kubernetes
k8s学习笔记之StorageClass+NFS
k8s学习笔记之StorageClass+NFS