1. 引言
本文将深入探讨一种提升并发性能的经典技术——锁分段(Lock Striping)。它是一种实现细粒度同步的模式,能够在保证线程安全的同时,显著提升多线程环境下对数据结构操作的吞吐量。
我们知道,在高并发场景下,粗粒度的锁机制往往会成为性能瓶颈。而 Lock Striping 正是为了解决这一问题而生。通过将锁的粒度从整个数据结构细化到“段”或“桶”,多个线程可以并行操作不同的段,从而减少竞争,提高并发效率。
2. 问题背景
HashMap
本身是非线程安全的,多线程同时读写可能导致数据不一致甚至结构破坏(如死循环)。✅
为解决此问题,常见的做法有:
- 使用
Collections.synchronizedMap()
包装 - 直接使用
Hashtable
⚠️ 这两种方式虽然实现了线程安全,但采用的是 粗粒度同步(coarse-grained synchronization) ——即整个 Map 被一个全局锁保护。这意味着任意时刻只能有一个线程访问 Map,无论是读还是写,结果就是并发退化为串行执行,性能极低。
我们的目标很明确:✅
在保证线程安全的前提下,尽可能提升并发访问能力。
3. 锁分段(Lock Striping)原理
锁分段的核心思想是:将数据结构划分为多个“段”(stripes),每个段拥有独立的锁。当线程访问某个段时,只需获取对应段的锁,而不影响其他段的操作。
这种方式实现了细粒度同步(fine-grained synchronization),允许多个线程同时操作不同段的数据,大幅提升并发性能。
实现锁分段有两种极端策略:
- ✅ 每段一个锁:并发度最高,但内存开销大
- ❌ 所有段共用一个锁:内存省了,但又回到了粗粒度同步的老路
为了在性能与内存之间取得平衡,Guava 提供了 Striped
工具类。它能高效管理大量锁(或信号量),支持按需分配 ReentrantLock
或 Semaphore
,其设计思想与 ConcurrentHashMap
内部的分段锁机制一脉相承,但更加通用和灵活。
4. 实战示例
下面我们通过一个性能对比实验,直观感受锁分段带来的提升。
我们将对比四种组合:
数据结构 | 同步方式 | 类名 |
---|---|---|
HashMap | 单锁 | SingleLock |
ConcurrentHashMap | 单锁 | SingleLock |
HashMap | 锁分段 | StripedLock |
ConcurrentHashMap | 锁分段 | StripedLock |
实验中,多个线程并发执行 put 和 get 操作,我们观察不同策略下的吞吐量表现。
4.1. 依赖引入
使用 Guava 的 Striped
类,需添加以下 Maven 依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
4.2. 核心执行逻辑
定义抽象基类 ConcurrentAccessExperiment
,统一管理并发任务的调度:
public abstract class ConcurrentAccessExperiment {
public final Map<String, String> doWork(Map<String, String> map, int tasks, int slots) {
CompletableFuture<?>[] requests = new CompletableFuture<?>[tasks * slots];
for (int i = 0; i < tasks; i++) {
requests[slots * i + 0] = CompletableFuture.supplyAsync(putSupplier(map, i));
requests[slots * i + 1] = CompletableFuture.supplyAsync(getSupplier(map, i));
requests[slots * i + 2] = CompletableFuture.supplyAsync(getSupplier(map, i));
requests[slots * i + 3] = CompletableFuture.supplyAsync(getSupplier(map, i));
}
CompletableFuture.allOf(requests).join();
return map;
}
protected abstract Supplier<?> putSupplier(Map<String, String> map, int key);
protected abstract Supplier<?> getSupplier(Map<String, String> map, int key);
}
💡 说明:每个任务执行 1 次 put 和 3 次 get,模拟读多写少的典型场景。任务数与 CPU 核心数匹配,避免过度线程竞争。
4.3. 单锁实现(SingleLock)
使用一个 ReentrantLock
保护整个 Map:
public class SingleLock extends ConcurrentAccessExperiment {
ReentrantLock lock;
public SingleLock() {
lock = new ReentrantLock();
}
protected Supplier<?> putSupplier(Map<String, String> map, int key) {
return (()-> {
lock.lock();
try {
return map.put("key" + key, "value" + key);
} finally {
lock.unlock();
}
});
}
protected Supplier<?> getSupplier(Map<String, String> map, int key) {
return (()-> {
lock.lock();
try {
return map.get("key" + key);
} finally {
lock.unlock();
}
});
}
}
⚠️ 踩坑提醒:即使使用 ConcurrentHashMap
,若外部加了单锁,其内部并发机制也完全失效,性能等同于同步 Map。
4.4. 锁分段实现(StripedLock)
使用 Guava 的 Striped
为每个“桶”分配独立锁:
public class StripedLock extends ConcurrentAccessExperiment {
Striped<Lock> lock; // 注意泛型
public StripedLock(int buckets) {
lock = Striped.lock(buckets);
}
protected Supplier<?> putSupplier(Map<String, String> map, int key) {
return (()-> {
int bucket = key % lock.size(); // 计算所属桶
Lock lock = this.lock.get(bucket);
lock.lock();
try {
return map.put("key" + key, "value" + key);
} finally {
lock.unlock();
}
});
}
protected Supplier<?> getSupplier(Map<String, String> map, int key) {
return (()-> {
int bucket = key % lock.size();
Lock lock = this.lock.get(bucket);
lock.lock();
try {
return map.get("key" + key);
} finally {
lock.unlock();
}
});
}
}
✅
Striped.lock(n)
会创建 n 个锁,通过哈希或模运算将 key 映射到具体锁,实现隔离。
5. 性能测试结果
使用 JMH(Java Microbenchmark Harness) 进行压测,结果如下(单位:ops/ms,越高越好):
Benchmark Mode Cnt Score Error Units
ConcurrentAccessBenchmark.singleLockConcurrentHashMap thrpt 10 0,059 ± 0,006 ops/ms
ConcurrentAccessBenchmark.singleLockHashMap thrpt 10 0,061 ± 0,005 ops/ms
ConcurrentAccessBenchmark.stripedLockConcurrentHashMap thrpt 10 0,065 ± 0,009 ops/ms
ConcurrentAccessBenchmark.stripedLockHashMap thrpt 10 0,068 ± 0,008 ops/ms
📊 结果分析:
- 单锁模式下,
HashMap
与ConcurrentHashMap
性能几乎一致 —— 因为锁粒度已覆盖整个操作 - 使用锁分段后,**性能提升约 10%~12%**,且
HashMap + StripedLock
组合表现最佳 - 说明:在可控并发场景下,手动细粒度锁可优于内置并发结构的默认策略
6. 总结
锁分段是一种简单粗暴但非常有效的并发优化手段。通过降低锁的粒度,让多线程真正“并行”起来,避免不必要的串行化等待。
本文通过对比实验证明:
- ❌ 粗粒度锁是并发性能的“隐形杀手”
- ✅
Striped
工具类让实现锁分段变得极其简单 - ✅ 在读多写少、访问分布均匀的场景下,锁分段收益明显
🔗 示例代码已托管至 GitHub:https://github.com/tech-tutorial/core-java-concurrency/tree/main/lock-striping
实际项目中,若发现 ConcurrentHashMap
仍存在热点竞争(如某些 key 被频繁访问),可考虑结合 Striped
进一步拆分锁,轻松完成性能调优。