引言在多线程环境下如何高效且安全地管理共享数据是并发编程的核心挑战之一。ConcurrentHashMap作为 Java 并发包java.util.concurrent中的重要组件提供了线程安全的哈希表实现广泛应用于高并发场景。与传统的HashMap和Hashtable不同ConcurrentHashMap通过分段锁、CAS 操作和精细化的并发控制机制在保证线程安全的同时极大地提升了性能。本文将深入探讨ConcurrentHashMap的核心实现原理包括其关键字段sizeCtl的作用、put和get方法的线程安全机制、扩容原理transfer方法以及size方法的实现细节。通过对这些内容的分析读者将能够全面理解ConcurrentHashMap的设计思想及其在高并发场景下的优势。1. ConrrentHashMap中的sizeCtlConrrentHashMap 执行构造方法和数组初始化是分开的数组初始化延迟到第一次执行插入操作。当sizeCtl 0时表示使用的是无参构造器此时数组尚未创建默认初始容量为 16在第一次插入元素时才会真正初始化。当sizeCtl 0时需要区分数组的状态如果数组还未创建即使用了有参构造器那么这个正数代表用户指定的初始容量注意实际存储的值会被调整为大于等于指定容量的最小 2 的幂次例如指定 100内部会变成 128如果数组已经创建完毕则表示下一次扩容的阈值通常等于当前数组容量乘以负载因子 0.75。当sizeCtl -1时表示数组正在初始化其他线程需要自旋等待保证只有一个线程执行初始化工作。当sizeCtl -1时说明数组正在扩容。此时sizeCtl被拆分成两部分高位存储着一个扩容戳由当前数组长度通过resizeStamp方法计算得到用于标识本次扩容的唯一性低位存储着参与扩容的线程数加一。例如(resizeStamp 16) 2表示有一个线程正在执行扩容每增加一个参与的线程低位的数值就会增加一。扩容完成后sizeCtl会被重新设置为新数组的扩容阈值正数容量标识生成sizeCtl resizeStamp(tab.length) RESIZE_STAMP_SHIFT;2. ConrrentHashMap.put()先计算key的hash值。如果数组为空初始化数组。如果当前hash槽对应的值为null则已CAS方式在数组上添加数据NodeK,V(hash, key, value)。如果当前hash槽的hash值为MOVE值等于-1说明是ForwardingNode节点说明在进行扩容则当前线程帮助扩容完。否则锁住当前hash槽头节点将数据添加到链表或者红黑树hash值0是链表如果是链表还需要记录链表长度。如果链表长度8时并且数组长度64则将链表转为红黑树。当红黑树长度6时将红黑树转为链表。3. ConrrentHashMap.initTable()判断数组是否未初始化如果未初始化则进行初始化。table被volitale修饰。判断sizeCtl的值是否0是的话则发现有其他线程在做数据的初始化并让出CPU。否则通过CAS将sizeCtl赋值为-1。再次判断数组是否未初始化如果未初始化则进行初始化。根据sizeCtl初始化hash表并且设置sizeCtlsizeCtl*0.75扩容阈值。4. ConcurrentHashMap.transfer()扩容原理判断是否已经在扩容当put等操作发现元素个数超过阈值 (sizeCtl)且数组未处于扩容中时会触发扩容。首先创建扩容戳resizeStamp(n)并尝试通过 CAS 将sizeCtl从正数改为(resizeStamp 16) 2低16位为2表示有一个线程正在扩容。若 CAS 成功该线程成为第一个发起扩容的线程。创建新数组只有第一个发起扩容的线程会创建新数组nextTable长度为原数组的 2 倍。其他帮助扩容的线程直接使用已创建好的nextTable。任务分片多线程协作根据 CPU 核数和原数组长度计算每个线程负责的桶数stride (NCPU 1) ? (n 3) / NCPU : n且最小为 16。共享变量transferIndex初始指向原数组长度n。每个线程通过 CAS 将transferIndex减去stride获得区间[transferIndex - stride, transferIndex)的桶任务。例如transferIndex从 64 → 48 → 32 …线程领取任务后便处理该区间内的桶。处理单个桶线程遍历自己负责的每个桶若桶为空 → 直接放置ForwardingNodehash MOVED,nextTable指向新数组。若桶非空 → 对头节点加synchronized锁将桶内的所有节点重新散列到新数组的低位原索引或高位原索引 原数组长度。迁移完成后将旧桶位置替换为ForwardingNodeCAS 确保原子性。线程完成当前任务后线程处理完自己领取的所有桶后会再次尝试获取新任务。若transferIndex 0表示所有桶都已被领取但可能仍有其他线程正在处理中。此时该线程通过 CAS 将sizeCtl的低 16 位减 1表示自己的扩容工作完成并检查是否还有活跃的扩容线程。最后一个线程的收尾工作当某个线程完成自己的任务且发现sizeCtl的低 16 位减到初始值2即只剩自己这个线程时它会再次扫描整个数组finishing标志为true确认所有桶都已被ForwardingNode覆盖。确认无误后将table指向新数组nextTable并重新设置sizeCtl 新数组长度 * 0.75扩容阈值。至此扩容完成。ConcurrentHashMap源码解析多线程扩容 - Life_Goes_On - 博客园5. ConcurrentHashMap.addCount()优先通过CAS往baseCount上累加成功则累加上失败则走2。先判断是否存在累加数组如果不存在则创建长度为1的累加数组。如果存在累加数组则优先遍历现有累加数组进行CAS自增如果所有的累加数组都CAS自增失败则对旧的累加数组进行扩容为2倍大小。6. ConcurrentHashMap进行put的时候如何保证线程安全先判断hash表是否为空未创建如果为空则通过CAS的方式创建hash表。判断hash值求得桶下标如果槽位的head空则通过CAS添加到head如果CAS失败则走3。通过synchronized锁住当前节点的链表进行元素添加。7. ConcurrentHashMap.get()计算key对应的hash值取到其所在数组的下标位置。如果头节点的hash值和计算的hash值以及头节点的key和传进来的key相同则直接头节点的值。如果头节点的hash值为负数说明正在扩容-1或者红黑树-2调用各自的find()进行查找。否则就是链表遍历链表逐个确认。如果是ForwardingNode节点会根据实际的数据结构进行查找可以是ForwardingNode、红黑树、链表。7.1 ConcurrentHashMap.get()为什么不加锁table数组Node[]、Node.val都是volatile修饰的。volatile修饰会强制将修改的值立即写入主内存并且强制其他线程的工作内存中的缓存失效。保证可见性8. ConcurrentHashMap.size()对baseCount和累加数组求和。感谢您的阅读如果文章中有任何问题或不足之处欢迎及时指出您的反馈将帮助我不断改进与完善。期待与您共同探讨技术共同进步