在Java并发编程领域,ConcurrentHashMap无疑是最核心的容器之一。作为HashMap的线程安全替代品,它既解决了Hashtable全表锁导致的性能瓶颈,又规避了HashMap在并发环境下的数据不一致风险(如死循环、数据丢失)。自JDK 1.5引入以来,ConcurrentHashMap经历了三次重大演进(JDK 7分段锁、JDK 8 CAS+synchronized、JDK 17性能优化),其设计思想堪称并发编程的典范。本文将从数据结构、并发控制、源码实现到实战应用,全方位剖析这一"并发神器"。
核心设计:
JDK 7的ConcurrentHashMap采用分段锁(Segment) 机制,将数据划分为16个独立的Segment(默认并发度16),每个Segment本质是一个"小Hashtable",继承自ReentrantLock。线程访问时,先通过key的hash定位到Segment,再对该Segment加锁,实现"不同段并行,同段串行"。
数据结构:
// JDK 7核心结构
class ConcurrentHashMap {
final Segment<K,V>[] segments; // 分段锁数组
transient int segmentMask; // 段掩码(用于定位Segment)
transient int segmentShift; // 段偏移量
}
class Segment<K,V> extends ReentrantLock {
transient HashEntry<K,V>[] table; // 每个Segment包含一个哈希表
transient int count; // 元素数量
}
局限性:
JDK 8彻底重构了ConcurrentHashMap,摒弃Segment,采用数组+链表+红黑树的结构,结合CAS无锁操作和synchronized细粒度锁,实现更高效的并发控制。
核心改进:
JDK 17作为长期支持版本(LTS),虽未改变ConcurrentHashMap的核心设计,但通过JVM层面的优化间接提升其性能:
JDK 8+的ConcurrentHashMap通过不同节点类型实现复杂功能,关键节点包括:
节点类型 | 作用场景 | 核心字段 |
---|---|---|
Node | 基础键值对节点(链表) | final int hash; final K key; volatile V val; volatile Node |
TreeBin | 红黑树容器节点 | TreeNode |
ForwardingNode | 扩容时的转发节点 | final Node (指向新数组) |
ReservationNode | computeIfAbsent等方法的占位节点 | volatile Object value; (标记正在计算中) |
触发条件:当链表长度>8且数组长度≥64时,调用treeifyBin
方法转为红黑树;若数组长度<64,优先扩容而非转树。
阈值8的由来:根据泊松分布,链表长度为8的概率仅0.00000006(约千万分之六),此时转树可平衡查询性能与转换开销。源码如下:
// JDK 8 treeifyBin方法(链表转红黑树)
private 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) // 数组长度<64时优先扩容
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<>(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<>(hd)); // 替换为TreeBin节点
}
}
}
}
}
CAS(Compare-And-Swap)是ConcurrentHashMap实现无锁操作的核心,通过Unsafe类的原子指令保证操作的原子性。主要应用场景:
initTable
方法中CAS设置sizeCtl为-1);casTabAt
方法原子替换null为新Node);transfer
方法中CAS设置ForwardingNode)。示例:空桶插入的CAS操作
// putVal方法中插入空桶
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // CAS成功则插入完成,失败则重试(可能有其他线程已插入)
}
当哈希冲突导致桶非空时,ConcurrentHashMap对桶的头节点加synchronized锁,而非整个数组或链表。锁粒度降至单个桶,极大减少竞争。
示例:链表插入的同步逻辑
// putVal方法中处理非空桶
else {
V oldVal = null;
synchronized (f) { // 锁住头节点f
if (tabAt(tab, i) == f) { // 二次校验(防止其他线程已修改该桶)
if (fh >= 0) { // 链表节点(fh为头节点hash)
binCount = 1;
for (Node<K,V> e = f;; ++binCount) { // 遍历链表
K ek;
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent) e.val = value; // 覆盖旧值
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) { // 插入链表尾部
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
} else if (f instanceof TreeBin) { // 红黑树节点
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent) p.val = value;
}
}
}
}
// 检查是否需要转红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i);
if (oldVal != null) return oldVal;
break;
}
}
ConcurrentHashMap大量使用volatile修饰核心变量,确保多线程间的可见性:
table
数组:保证数组引用的可见性;Node.val
和Node.next
:保证节点值和链表后继的可见性;sizeCtl
:控制初始化和扩容的状态标识(-1表示初始化中,负数表示扩容中)。putVal方法是ConcurrentHashMap的灵魂,涵盖初始化、CAS插入、锁同步、扩容触发等关键步骤。核心流程如下:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException(); // 不允许null键值
int hash = spread(key.hashCode()); // 二次哈希,减少冲突
int binCount = 0;
for (Node<K,V>[] tab = table;;) { // 自旋重试直到成功
Node<K,V> f; int n, i, fh;
// 1. 初始化数组(延迟初始化)
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 2. 空桶:CAS插入新节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
// 3. 扩容中:帮助迁移节点
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 4. 非空桶:加锁处理(链表/红黑树)
else {
V oldVal = null;
synchronized (f) { // 锁住头节点
// ... 链表/红黑树插入逻辑(见2.2节)...
}
// 5. 检查是否需要转红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null) return oldVal;
break;
}
}
}
// 6. 更新元素计数并触发扩容
addCount(1L, binCount);
return null;
}
当元素数量超过阈值(sizeCtl
,默认为数组长度的0.75倍)时,触发扩容。ConcurrentHashMap支持多线程并发扩容,迁移效率极高。
核心步骤:
nextTab
);i
和i+n
);关键代码片段:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 根据CPU核心数计算每个线程迁移的桶数(最少16个)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
if (nextTab == null) { // 初始化新数组
try {
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { sizeCtl = Integer.MAX_VALUE; return; }
nextTable = nextTab;
transferIndex = n; // 迁移起始位置(从旧数组尾部开始)
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<>(nextTab); // 转发节点
boolean advance = true;
boolean finishing = false; // 是否完成所有迁移
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 1. 分配迁移区间(i为当前桶索引,bound为区间下界)
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing) advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1; advance = false;
}
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride) ? nextIndex - stride : 0)) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 2. 迁移节点(加锁、拆分链表/红黑树)
if (i < 0 || i >= n || i + n >= nextn) {
// ... 迁移完成,更新table引用 ...
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd); // 空桶标记为转发节点
else if ((fh = f.hash) == MOVED)
advance = true; // 已迁移,跳过
else {
synchronized (f) { // 锁住旧桶头节点
if (tabAt(tab, i) == f) {
// ... 拆分节点到新数组的i和i+n桶 ...
}
}
}
}
}
为避免多线程竞争导致的计数性能瓶颈,ConcurrentHashMap使用CounterCell数组(类似LongAdder)存储元素数量:
baseCount
累加;addCount方法核心逻辑:
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended); // 竞争激烈时扩容CounterCell数组
return;
}
if (check <= 1) return;
s = sumCount(); // 累加baseCount和所有CounterCell的值
}
// ... 检查是否需要扩容 ...
}
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapBasic {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
// 1. 插入元素(put)
chm.put("a", 1);
chm.putIfAbsent("b", 2); // 原子操作:不存在则插入
// 2. 查询元素(get)
System.out.println(chm.get("a")); // 输出1
// 3. 更新元素(replace)
chm.replace("a", 1, 3); // 原子操作:仅当旧值为1时替换为3
// 4. 删除元素(remove)
chm.remove("b", 2); // 原子操作:仅当值为2时删除
System.out.println(chm); // 输出{a=3}
}
}
利用compute
方法实现线程安全的计数器,避免传统get-then-put
导致的竞态条件:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentCounter {
private final ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
// 原子递增计数器
public void increment(String key) {
counts.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
}
// 获取计数值
public int getCount(String key) {
return counts.getOrDefault(key, 0);
}
public static void main(String[] args) throws InterruptedException {
ConcurrentCounter counter = new ConcurrentCounter();
int threads = 8;
int iterations = 10000;
// 多线程并发递增
Runnable task = () -> {
for (int i = 0; i < iterations; i++) {
counter.increment("test");
}
};
Thread[] threadPool = new Thread[threads];
for (int i = 0; i < threads; i++) {
threadPool[i] = new Thread(task);
threadPool[i].start();
}
for (Thread t : threadPool) t.join();
System.out.println("最终计数:" + counter.getCount("test")); // 输出80000(无并发问题)
}
}
结合ScheduledExecutorService
实现自动过期的缓存,利用ConcurrentHashMap的原子方法保证线程安全:
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
public class ExpiringCache<K, V> {
private final ConcurrentHashMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();
private final long defaultTTL; // 默认过期时间(毫秒)
public ExpiringCache(long defaultTTL) {
this.defaultTTL = defaultTTL;
// 定时清理过期条目(每5秒执行一次)
cleaner.scheduleAtFixedRate(this::cleanExpired, 5, 5, TimeUnit.SECONDS);
}
// 存入缓存(原子操作)
public void put(K key, V value) {
cache.put(key, new CacheEntry<>(value, System.currentTimeMillis() + defaultTTL));
}
// 获取缓存(自动处理过期)
public V get(K key) {
CacheEntry<V> entry = cache.get(key);
if (entry == null || entry.isExpired()) {
cache.remove(key); // 移除过期条目
return null;
}
return entry.value;
}
// 清理过期条目
private void cleanExpired() {
long now = System.currentTimeMillis();
cache.entrySet().removeIf(e -> e.getValue().expireTime < now);
}
// 缓存条目(包含值和过期时间)
private static class CacheEntry<V> {
final V value;
final long expireTime;
CacheEntry(V value, long expireTime) {
this.value = value;
this.expireTime = expireTime;
}
boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
public static void main(String[] args) throws InterruptedException {
ExpiringCache<String, String> cache = new ExpiringCache<>(3000); // 3秒过期
cache.put("user", "Alice");
System.out.println(cache.get("user")); // 输出Alice
Thread.sleep(4000);
System.out.println(cache.get("user")); // 输出null(已过期)
}
}
容器类型 | 并发控制方式 | 读性能 | 写性能(高并发) | 锁粒度 |
---|---|---|---|---|
Hashtable | synchronized全表锁 | 低 | 极低(串行) | 整个表 |
Collections.synchronizedMap | synchronized全表锁 | 低 | 极低(串行) | 整个表 |
ConcurrentHashMap | CAS+synchronized桶锁 | 高(无锁) | 高(并行) | 单个桶 |
测试数据(4核CPU,10线程并发读写100万次):
JDK 8相比JDK 7性能提升约30%,主要得益于锁粒度降低和无锁读操作;JDK 17通过JVM优化进一步提升约15%吞吐量(尤其在大内存场景下)。
putIfAbsent
、compute
等原子方法的场景。equals
方法耗时过长,会导致synchronized锁持有时间增加,降低并发性能。ConcurrentHashMap的演进史,是Java并发编程从"粗粒度锁"到"无锁化+细粒度锁"的缩影。其成功源于:
在JDK 17及未来版本中,随着JVM和编译器的持续优化,ConcurrentHashMap仍将是高并发场景下的首选容器。掌握其原理与实践,是Java开发者深入并发编程的必经之路。