ConcurrentHashMap概述
支持检索的完全并发和可调整的预期更新并发的哈希表。
此类遵循与 Hashtable 相同的功能规范,并包含与 Hashtable 的每个方法对应的方法版本。
但是,即使所有操作都是线程安全的,检索操作也不需要锁定,并且不支持以阻止所有访问的方式锁定整个表。
在依赖线程安全但不依赖同步细节的程序中,此类与 Hashtable 完全可互操作。检索操作(包括 get)一般不会阻塞,因此可能与更新操作(包括 put 和 remove)重叠。
检索反映了最近完成的更新操作在其开始时保持的结果。对于 putAll 和 clear 等聚合操作,并发检索可能仅反映插入或删除某些条目。类似地,Iterators 和 Enumerations 返回的元素反映了在 iteratorenumeration 创建之时或之后的某个时刻哈希表的状态。
它们不会抛出 ConcurrentModificationException。
但是,迭代器被设计为一次只能由一个线程使用。更新操作之间允许的并发性由可选的 concurrencyLevel 构造函数参数(默认为 16)指导,该参数用作内部大小调整的提示。
该表在内部进行了分区,以尝试允许指定数量的并发更新而不会发生争用。
因为哈希表中的放置本质上是随机的,所以实际的并发性会有所不同。理想情况下,您应该选择一个值来容纳尽可能多的线程同时修改表。使用显着高于您需要的值会浪费空间和时间,而显着降低的值会导致线程争用。
但是一个数量级内的高估和低估通常不会产生太大的影响。当已知只有一个线程会修改而所有其他线程只会读取时,值 1 是合适的。此外,调整这种或任何其他类型的哈希表的大小是一个相对较慢的操作,因此,如果可能,最好在构造函数中提供预期表大小的估计值。
不允许null键和null值。
JDK 1.7
基本策略是将表细分为 Segments
,每个 Segment
本身就是一个并发可读的哈希表。为了减少占用空间,仅在第一次需要时才构建除一个段之外的所有段(请参阅 ensureSegment)。(segments[0]是在初始化ConcurrentHashMap时就构建好的
)
为了在存在惰性构造的情况下保持可见性,对段以及段表元素的访问必须使用 volatile 访问,这是通过下面的方法 segmentAt 等中的 Unsafe 完成的。这些提供了 AtomicReferenceArrays 的功能,但降低了间接级别。此外,锁定操作中的表元素和条目“next”字段的volatile-writes
使用更便宜的“lazySet”写入形式(通过 putOrderedObject),因为这些写入始终伴随着锁释放,以保持表更新的顺序一致性。
默认参数
| |
| static final int DEFAULT_INITIAL_CAPACITY = 16; |
| |
| static final float DEFAULT_LOAD_FACTOR = 0.75f; |
| |
| static final int DEFAULT_CONCURRENCY_LEVEL = 16; |
| |
| static final int MAXIMUM_CAPACITY = 1 << 30; |
| |
| static final int MIN_SEGMENT_TABLE_CAPACITY = 2; |
| |
| static final int MAX_SEGMENTS = 1 << 16; |
| |
| |
| final int segmentMask; |
| |
| final int segmentShift; |
| |
| final Segment<K,V>[] segments; |
构造方法
初始化操作: 段数组、段数组第一个元素,段偏移量
| |
| |
| 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; |
| |
| int sshift = 0; |
| int ssize = 1; |
| while (ssize < concurrencyLevel) { |
| ++sshift; |
| ssize <<= 1; |
| } |
| 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); |
| this.segments = ss; |
| } |
| |
| |
| public ConcurrentHashMap(int initialCapacity, float loadFactor) { |
| this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL); |
| } |
| |
| |
| public ConcurrentHashMap(int initialCapacity) { |
| this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); |
| } |
| |
| public ConcurrentHashMap() { |
| this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); |
| } |
Segment
段,是ReentrantLock的子类(只是为了投机取巧,简化一些锁定的操作和避免单独构造),也叫分段锁,是实现并发的关键。
段维护一个entry 列表,该表始终保持一致状态,因此可以在没有锁定的情况下读取(通过段和表的易失性读取)。
这需要在表调整大小期间在必要时复制节点,因此仍然使用旧版本表的读者可以遍历旧列表。
此类仅定义需要锁定的可变方法。除非另有说明,否则此类的方法执行 ConcurrentHashMap 方法的每段版本。 (其他方法直接集成到 ConcurrentHashMap 方法中。)
这些可变方法使用一种通过方法 scanAndLock
和 scanAndLockForPut
控制争用旋转的形式。
这些散布在tryLocks
中的遍历来定位节点。主要好处是在获取锁的同时吸收缓存未命中(这对于哈希表来说很常见),因此一旦获取,遍历速度就会更快。
我们实际上并没有使用找到的节点,因为无论如何都必须在锁定状态下重新获取它们以确保更新的顺序一致性(并且在任何情况下都可能无法检测到陈旧),但它们通常会更快地重新定位。此外,如果没有找到节点,scanAndLockForPut
会推测性地创建一个新节点以在 put
中使用。
| |
| static final class Segment<K,V> extends ReentrantLock implements Serializable { |
| |
| |
| transient volatile HashEntry<K,V>[] table; |
| |
| |
| transient int count; |
| |
| |
| transient int modCount; |
| |
| |
| transient int threshold; |
| |
| |
| final float loadFactor; |
| |
| Segment(float lf, int threshold, HashEntry<K,V>[] tab) { |
| this.loadFactor = lf; |
| this.threshold = threshold; |
| this.table = tab; |
| } |
| } |
put操作流程
| public V put(K key, V value) { |
| Segment<K,V> s; |
| if (value == null) |
| throw new NullPointerException(); |
| int hash = hash(key); |
| int j = (hash >>> segmentShift) & segmentMask; |
| |
| if ((s = (Segment<K,V>)UNSAFE.getObject |
| (segments, (j << SSHIFT) + SBASE)) == null) |
| s = ensureSegment(j); |
| return s.put(key, hash, value, false); |
| } |
| |
| |
| private Segment<K,V> ensureSegment(int k) { |
| final Segment<K,V>[] ss = this.segments; |
| long u = (k << SSHIFT) + SBASE; |
| Segment<K,V> seg; |
| if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { |
| Segment<K,V> proto = ss[0]; |
| int cap = proto.table.length; |
| float lf = proto.loadFactor; |
| int threshold = (int)(cap * lf); |
| HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap]; |
| if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) |
| == null) { |
| Segment<K,V> s = new Segment<K,V>(lf, threshold, tab); |
| while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) |
| == null) { |
| if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) |
| break; |
| } |
| } |
| } |
| return seg; |
| } |
Segment put操作
核心就是 tryLock
和 scanAndLockForPut
| final V put(K key, int hash, V value, boolean onlyIfAbsent) { |
| HashEntry<K,V> node = tryLock() ? null : |
| scanAndLockForPut(key, hash, value); |
| V oldValue; |
| try { |
| HashEntry<K,V>[] tab = table; |
| int index = (tab.length - 1) & hash; |
| HashEntry<K,V> first = entryAt(tab, index); |
| for (HashEntry<K,V> e = first;;) { |
| |
| if (e != null) { |
| K k; |
| if ((k = e.key) == key || |
| (e.hash == hash && key.equals(k))) { |
| oldValue = e.value; |
| if (!onlyIfAbsent) { |
| e.value = value; |
| ++modCount; |
| } |
| break; |
| } |
| e = e.next; |
| } |
| |
| else { |
| if (node != null) |
| node.setNext(first); |
| else |
| |
| |
| node = new HashEntry<K,V>(hash, key, value, first); |
| int c = count + 1; |
| if (c > threshold && tab.length < MAXIMUM_CAPACITY) |
| rehash(node); |
| else |
| setEntryAt(tab, index, node); |
| ++modCount; |
| count = c; |
| oldValue = null; |
| break; |
| } |
| } |
| } finally { |
| unlock(); |
| } |
| return oldValue; |
| } |
| |
| |
| |
| |
| |
| private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) { |
| HashEntry<K,V> first = entryForHash(this, hash); |
| HashEntry<K,V> e = first; |
| HashEntry<K,V> node = null; |
| int retries = -1; |
| while (!tryLock()) { |
| HashEntry<K,V> f; |
| if (retries < 0) { |
| if (e == null) { |
| if (node == null) |
| node = new HashEntry<K,V>(hash, key, value, null); |
| retries = 0; |
| } |
| else if (key.equals(e.key)) |
| retries = 0; |
| else |
| e = e.next; |
| } |
| else if (++retries > MAX_SCAN_RETRIES) { |
| lock(); |
| break; |
| } |
| else if ((retries & 1) == 0 && |
| (f = entryForHash(this, hash)) != first) { |
| e = first = f; |
| retries = -1; |
| } |
| } |
| return node; |
| } |
| |
| |
| private void rehash(HashEntry<K,V> node) { |
| HashEntry<K,V>[] oldTable = table; |
| int oldCapacity = oldTable.length; |
| int newCapacity = oldCapacity << 1; |
| threshold = (int)(newCapacity * loadFactor); |
| HashEntry<K,V>[] newTable = |
| (HashEntry<K,V>[]) new HashEntry[newCapacity]; |
| int sizeMask = newCapacity - 1; |
| for (int i = 0; i < oldCapacity ; i++) { |
| HashEntry<K,V> e = oldTable[i]; |
| if (e != null) { |
| HashEntry<K,V> next = e.next; |
| int idx = e.hash & sizeMask; |
| if (next == null) |
| newTable[idx] = e; |
| else { |
| HashEntry<K,V> lastRun = e; |
| int lastIdx = idx; |
| for (HashEntry<K,V> last = next; |
| last != null; |
| last = last.next) { |
| int k = last.hash & sizeMask; |
| if (k != lastIdx) { |
| lastIdx = k; |
| lastRun = last; |
| } |
| } |
| newTable[lastIdx] = lastRun; |
| |
| for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { |
| V v = p.value; |
| int h = p.hash; |
| int k = h & sizeMask; |
| HashEntry<K,V> n = newTable[k]; |
| newTable[k] = new HashEntry<K,V>(h, p.key, v, n); |
| } |
| } |
| } |
| } |
| int nodeIndex = node.hash & sizeMask; |
| node.setNext(newTable[nodeIndex]); |
| newTable[nodeIndex] = node; |
| table = newTable; |
| } |
put 流程总结

remove 操作
| public V remove(Object key) { |
| int hash = hash(key); |
| Segment<K,V> s = segmentForHash(hash); |
| return s == null ? null : s.remove(key, hash, null); |
| } |
| |
| private Segment<K,V> segmentForHash(int h) { |
| long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; |
| return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u); |
| } |
| |
Segment remove操作
| final V remove(Object key, int hash, Object value) { |
| if (!tryLock()) |
| scanAndLock(key, hash); |
| V oldValue = null; |
| try { |
| HashEntry<K,V>[] tab = table; |
| int index = (tab.length - 1) & hash; |
| HashEntry<K,V> e = entryAt(tab, index); |
| HashEntry<K,V> pred = null; |
| while (e != null) { |
| K k; |
| HashEntry<K,V> next = e.next; |
| if ((k = e.key) == key || |
| (e.hash == hash && key.equals(k))) { |
| V v = e.value; |
| if (value == null || value == v || value.equals(v)) { |
| if (pred == null) |
| setEntryAt(tab, index, next); |
| else |
| pred.setNext(next); |
| ++modCount; |
| --count; |
| oldValue = v; |
| } |
| break; |
| } |
| pred = e; |
| e = next; |
| } |
| } finally { |
| unlock(); |
| } |
| return oldValue; |
| } |
| |
| |
| |
| private void scanAndLock(Object key, int hash) { |
| HashEntry<K,V> first = entryForHash(this, hash); |
| HashEntry<K,V> e = first; |
| int retries = -1; |
| while (!tryLock()) { |
| HashEntry<K,V> f; |
| if (retries < 0) { |
| if (e == null || key.equals(e.key)) |
| retries = 0; |
| else |
| e = e.next; |
| } |
| else if (++retries > MAX_SCAN_RETRIES) { |
| lock(); |
| break; |
| } |
| else if ((retries & 1) == 0 && |
| (f = entryForHash(this, hash)) != first) { |
| e = first = f; |
| retries = -1; |
| } |
| } |
| } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)