锁分离通过为共享资源的独立子部分分配细粒度锁来提升并发性能,如ConcurrentHashMap按桶分锁;需确保子部分无交叉修改,避免原子性破坏,并注意哈希冲突、负哈希值、锁内耗时操作等常见错误。

锁分离的核心思路是拆分单一锁为多个细粒度锁
当多个线程频繁争抢同一把锁(比如 synchronized 修饰的整个方法或 ReentrantLock 保护的大段逻辑),吞吐量会急剧下降。锁分离不是“换锁”,而是识别出共享资源中可独立访问的子部分,为每个子部分分配独立锁。典型例子是 ConcurrentHashMap:它不锁整个哈希表,而是按 segment(Java 8 后改为 Node 数组的桶区间)划分,写操作只锁对应桶链/红黑树的头节点。
实操时需注意:拆锁的前提是各子部分之间无交叉修改——比如两个线程分别操作 list.get(0) 和 list.get(1),但若底层是 ArrayList,其 set() 方法仍可能触发数组扩容并影响其他索引,此时不能简单按索引分锁。
用 ReentrantLock 数组实现按 key 哈希分锁
适用于自定义缓存、计数器等 key-value 场景,避免全局锁成为瓶颈:
private static final int LOCK_SIZE = 64; private final ReentrantLock[] locks = new ReentrantLock[LOCK_SIZE]; private final Mapdata = new HashMap<>(); public Counter() { for (int i = 0; i < LOCK_SIZE; i++) { locks[i] = new ReentrantLock(); } } private int getLockIndex(String key) { return Math.abs(key.hashCode()) % LOCK_SIZE; } public void increment(String key) { int idx = getLockIndex(key); locks[idx].lock(); try { data.merge(key, 1, Integer::sum); } finally { locks[idx].unlock(); } }
常见错误:
立即学习“Java免费学习笔记(深入)”;
- LOCK_SIZE 过小(如设为 4)导致哈希冲突高,多个 key 挤在同一个锁上,实际未缓解竞争
- 直接用
key.hashCode() % LOCK_SIZE,忽略负数哈希值引发数组越界 - 在锁内执行耗时操作(如远程调用、IO),延长锁持有时间,反而恶化整体延迟
读写锁分离:ReadWriteLock 不等于自动高性能
ReentrantReadWriteLock 允许多个读线程并发,但写线程独占——这只有在「读多写少 + 读操作轻量」时才真正有效。一旦写操作频繁,或读操作本身很重(如遍历大集合、序列化),读锁的共享优势会被抵消,甚至因锁升级失败、写线程饥饿而更慢。
使用建议:
- 仅对明确满足「读远大于写」且读逻辑极快的场景使用,例如配置项缓存
- 避免在读锁内调用可能升级为写锁的方法(如
ConcurrentHashMap.computeIfAbsent()在读锁中触发计算,可能阻塞写线程) - 不要用
StampedLock的乐观读代替所有场景:它的validate()失败后要回退到悲观读,若失败率高,开销反超ReentrantReadWriteLock
锁分离后必须检查复合操作的原子性是否被破坏
拆锁提升并发度的同时,天然牺牲了跨资源的原子性。例如将用户账户余额和交易流水分别用不同锁保护,就无法保证「扣款 + 记流水」的事务性。这时需要额外机制补偿:
- 业务层引入幂等标识 + 状态机(如交易单从
PENDING→CONFIRMED) - 使用本地消息表或可靠消息队列实现最终一致性
- 必要时退回到分布式锁(如 Redis 的
SETNX)或数据库行锁,但要评估延迟代价
最容易被忽略的是:锁分离后单元测试往往只覆盖单 key 路径,漏掉多 key 协同失败的边界情况,比如转账时 A 扣款成功、B 入账失败,最终状态不一致。











