进阶篇
文章主要针对上篇中方案3的进阶进行升级对比介绍
WeakKeyLockManager版本
使用 WeakHashMap + ReentrantLock 实现按 Key(如 uid_cid)自动回收的锁池(Lock Pool),适用于需要对不同 key 粒度加锁,又希望避免内存泄漏的场景。
✅ 1. WeakKeyLockManager(支持按 key 自动回收的锁)
java
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReentrantLock;
public class WeakKeyLockManager<K> {
// 用于保护 WeakHashMap 本身的线程安全
private final Object mapLock = new Object();
// 使用 WeakHashMap 自动回收 key 不再引用的锁
private final WeakHashMap<K, ReentrantLock> lockMap = new WeakHashMap<>();
public ReentrantLock getLock(K key) {
// 为什么要全局加锁?防止一个key对应多个锁。以及容易踩坑
synchronized (mapLock) {
return lockMap.computeIfAbsent(key, k -> new ReentrantLock());
}
}
}
synchronized(key)容易踩坑案例
java
String key1 = new String("123_456"); // new 出来的对象
String key2 = new String("123_456");
System.out.println(key1 == key2); // false!!
这两个 synchronized 是完全独立的两个锁,不会互斥!
所以 synchronized(key) 只在你100% 保证 key 是相同对象引用时 才有效(注意,不是 equals 相等,而是 引用相等:==)。
更隐蔽的情况:即使你用的是 "123_456" 字面量
java
String k1 = "123_456"; // 字面量常量池
String k2 = "123" + "_" + "456"; // 编译期优化后也一样
System.out.println(k1 == k2); // true ✅
synchronized (k1) {
// 线程A
}
synchronized (k2) {
// 线程B,能互斥 ✅
}
这在编译期是同一个对象引用,互斥有效。
❗ 但如果是运行时拼接,就失效了:
java
String k1 = "123_456";
String k2 = String.valueOf(123) + "_" + String.valueOf(456);
System.out.println(k1 == k2); // false ❌
k1 != k2,synchronized(k1) 与 synchronized(k2)不会互斥。
使用intern
缺点:
- intern() 会把字符串放进 JVM 字符串常量池;
- key 数量多时容易造成 常量池内存泄漏/膨胀;
- 对高并发系统来说风险较大。
✅ 为什么这个结构安全?
WeakHashMap会在 key 没有外部强引用时自动移除 entry;- 保证了当
key(如uid_cid组合)不再使用后,对应的锁也能自动被 GC; - 使用
synchronized是为了保护lockMap多线程访问的安全(你也可以改用Collections.synchronizedMap(...)或显式ReadWriteLock);
✅ 2. 示例使用代码
java
public class UpsertService {
private final WeakKeyLockManager<String> lockManager = new WeakKeyLockManager<>();
public void upsert(Long uid, Long cid, YourDataDTO data) {
String key = uid + "_" + cid;
ReentrantLock lock = lockManager.getLock(key);
lock.lock();
try {
// 此处是数据库 upsert 的操作
System.out.println("Thread " + Thread.currentThread().getName() + " processing: " + key);
Thread.sleep(50); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
✅ 3. 测试类(模拟并发)
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LockTest {
public static void main(String[] args) {
UpsertService service = new UpsertService();
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 模拟多线程处理 3 个不同 key,并发冲突
for (int i = 0; i < 100; i++) {
final long uid = i % 3;
final long cid = i % 2;
threadPool.submit(() -> service.upsert(uid, cid, null));
}
threadPool.shutdown();
}
}
输出结果(简略):
plain
Thread pool-1-thread-1 processing: 1_1
Thread pool-1-thread-3 processing: 0_0
Thread pool-1-thread-2 processing: 2_1
Thread pool-1-thread-4 processing: 1_1 <-- 同一个 key 会排队执行
🧠 补充说明
| 项目 | 说明 |
|---|---|
| 锁是否自动释放? | 是的,WeakHashMap 中的 key(uid_cid )如果没有外部强引用,就会被 GC |
| 是否线程安全? | 是,getLock() 用了 synchronized ;如追求更高性能,可用 ConcurrentMap<K, WeakReference<ReentrantLock>> |
| 是否锁冲突少? | 是,每个 uid+cid 都能拥有自己的锁,精度高,冲突少 |
🚀 进阶(更高性能)
如果你业务量非常大,可以改为:
java
Map<K, WeakReference<ReentrantLock>> map = new ConcurrentHashMap<>();
- 每次
get()时判断ref.get()是否已被 GC。 - 避免同步块,提高并发性能。
进阶实现方案2
java
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
public class KeyLockManager<K> {
private static class LockWrapper<K> extends WeakReference<Object> {
final K key;
LockWrapper(K key, Object referent, ReferenceQueue<Object> queue) {
super(referent, queue);
this.key = key;
}
}
private final ConcurrentHashMap<K, LockWrapper<K>> lockMap = new ConcurrentHashMap<>();
private final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
public Object getLock(K key) {
cleanStaleEntries();
while (true) {
LockWrapper<K> wrapper = lockMap.get(key);
Object lock = wrapper != null ? wrapper.get() : null;
if (lock != null) {
return lock;
}
Object newLock = new Object();
LockWrapper<K> newWrapper = new LockWrapper<>(key, newLock, refQueue);
LockWrapper<K> existing = lockMap.putIfAbsent(key, newWrapper);
if (existing == null) {
return newLock;
}
Object existingLock = existing.get();
if (existingLock != null) {
return existingLock;
}
lockMap.replace(key, existing, newWrapper);
}
}
private void cleanStaleEntries() {
LockWrapper<K> ref;
while ((ref = (LockWrapper<K>) refQueue.poll()) != null) {
lockMap.remove(ref.key, ref);
}
}
}
使用方式
java
KeyLockManager<String> lockManager = new KeyLockManager<>();
public void upsert(String uid, String cid) {
String key = uid + "_" + cid;
Object lock = lockManager.getLock(key);
synchronized (lock) {
// 安全执行你的 upsert 操作
System.out.println(Thread.currentThread().getName() + " -> " + key);
}
}
将原有的mapLock替换为新的锁对象池获取:lockManager.getLock(key)。支持key粒度的锁提升并发。但是实现更复杂成本也相对下一个方案更高
✅ 总结
WeakKeyLockManager<K>利用了 Java 的 GC 特性,对 key 精准加锁且无内存泄漏风险;- 在高并发场景中,它是介于 "分段锁" 和 "全量锁池" 之间的优雅方案;
- 同时具备:高并发 + 自动清理 + 精确锁粒度 的优势。
线程安全、无锁实现
✅ 线程安全、无锁实现:基于 ConcurrentHashMap<WeakReference<ReentrantLock>> 的按 key 自动清理锁管理器
这是一个高性能、支持自动回收、避免锁泄露 的锁池方案 ------ 比 WeakHashMap 更适用于并发环境,推荐用于高并发下的"按 key 精准加锁"。
✅ 为什么要用 ConcurrentHashMap<WeakReference<Lock>>?
ConcurrentHashMap支持并发读写(比WeakHashMap更适合多线程);WeakReference<ReentrantLock>可以避免锁泄漏(key 无引用时,锁可以被回收);- 加上
ReferenceQueue,我们可以主动清理无效锁对象,避免锁池膨胀。
🚀 实现代码
🔧 WeakConcurrentLockManager<K> 类
java
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public class WeakConcurrentLockManager<K> {
private static class LockReference<K> extends WeakReference<ReentrantLock> {
final K key;
LockReference(K key, ReentrantLock referent, ReferenceQueue<ReentrantLock> q) {
super(referent, q);
this.key = key;
}
}
private final ConcurrentHashMap<K, LockReference<K>> lockMap = new ConcurrentHashMap<>();
private final ReferenceQueue<ReentrantLock> queue = new ReferenceQueue<>();
/**
* 获取指定 key 对应的锁,若不存在则创建。
*/
public ReentrantLock getLock(K key) {
cleanStaleLocks();
while (true) {
LockReference<K> ref = lockMap.get(key);
ReentrantLock existingLock = ref == null ? null : ref.get();
if (existingLock != null) {
return existingLock;
}
// 若当前无有效锁,则尝试创建新的锁
ReentrantLock newLock = new ReentrantLock();
LockReference<K> newRef = new LockReference<>(key, newLock, queue);
// putIfAbsent,防止多个线程同时创建同一个锁
LockReference<K> prev = lockMap.putIfAbsent(key, newRef);
if (prev == null) {
return newLock;
}
ReentrantLock prevLock = prev.get();
if (prevLock != null) {
return prevLock;
} else {
// 已有的锁被GC掉,尝试替换
lockMap.replace(key, prev, newRef);
}
}
}
/**
* 清理已被 GC 的锁引用
*/
private void cleanStaleLocks() {
LockReference<K> ref;
while ((ref = (LockReference<K>) queue.poll()) != null) {
lockMap.remove(ref.key, ref);
}
}
}
✅ 使用方式
java
public class UpsertService {
private final WeakConcurrentLockManager<String> lockManager = new WeakConcurrentLockManager<>();
public void upsert(String uid, String cid) {
String key = uid + "_" + cid;
ReentrantLock lock = lockManager.getLock(key);
lock.lock();
try {
// 这里处理 upsert 逻辑
System.out.println(Thread.currentThread().getName() + " processing: " + key);
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
✅ 测试代码(并发场景模拟)
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestApp {
public static void main(String[] args) {
UpsertService service = new UpsertService();
ExecutorService executor = Executors.newFixedThreadPool(8);
for (int i = 0; i < 100; i++) {
final String uid = String.valueOf(i % 3);
final String cid = String.valueOf(i % 2);
executor.submit(() -> service.upsert(uid, cid));
}
executor.shutdown();
}
}
✅ 输出示例(说明同一个 key 串行)
plain
pool-1-thread-1 processing: 1_1
pool-1-thread-3 processing: 0_0
pool-1-thread-2 processing: 2_1
...
✅ 优点总结
| 特性 | 说明 |
|---|---|
| 🔒 精确按 key 加锁 | 每个 key 单独一把锁 |
| 🧠 自动回收锁 | 使用 WeakReference ,key 无引用时锁自动清理 |
| ⚙️ 高性能 | 基于 ConcurrentHashMap ,适合并发环境 |
| ✅ 线程安全 | 所有锁获取和清理均线程安全 |
🚫 注意事项
key类型必须正确实现equals()和hashCode();- 如果你频繁创建短命 key,需要保证 key 外部也有引用(防止锁被 GC 太早);
- 如果你不希望锁被 GC,可以使用
ConcurrentHashMap<K, Lock>但要手动清理;
方案对比
非常好!我们现在来深入对比 你提到的两个加锁方案在"按 key 精准加锁"场景下的使用效果。
🎯 目标场景
并发环境下,业务根据 (uid, cid) 或封装后的 key 进行批量 upsert、避免写冲突,需要对每个 key 加锁,但不同 key 之间可并发。
🆚 对比双方
| 方案 | 简称 |
|---|---|
1️⃣ synchronized + Map<UniqueKey, Object> |
有锁方案(全局锁 map) |
2️⃣ ConcurrentHashMap<K, WeakReference<Lock>> + ReferenceQueue |
无锁方案(细粒度并发锁池) |
🧠 示例实现简述
✅ 有锁方案:synchronized + WeakHashMap
java
Map<UniqueKey, Object> lockMap = new WeakHashMap<>();
public synchronized Object getLock(UniqueKey key) {
return lockMap.computeIfAbsent(key, k -> new Object());
}
- 通过全局
**synchronized**确保**WeakHashMap**的线程安全 - 自动回收未使用的 key 对应的锁对象
synchronized (getLock(key))实现互斥
✅ 无锁方案:ConcurrentHashMap<K, WeakReference<ReentrantLock>> + 清理
java
// 封装类中
public ReentrantLock getLock(K key) {
// 使用 putIfAbsent / replace 保证线程安全
// 用 ReferenceQueue 清除已 GC 的锁
}
- 每个 key 映射一个
ReentrantLock,多线程安全创建 - 支持高并发、自动回收无效锁
- 使用
lock.lock()/lock.unlock()实现互斥
✅ 核心维度对比
| 对比维度 | synchronized + Map (全局锁) |
ConcurrentMap + WeakReference (无锁方案) |
|---|---|---|
| 🧵 线程安全性 | 依赖 synchronized 全局互斥,线程安全,但易阻塞 |
无需全局锁,使用原子操作 + 线程安全结构,线程安全 ✅ |
| 🚀 并发性能 | ❌ 差:获取锁需要串行进入 getLock() |
✅ 高:并发获取锁时几乎无冲突,适合多核、高并发 |
| 🧹 锁对象自动清理 | ✅ 是(借助 WeakHashMap ) |
✅ 是(WeakReference + ReferenceQueue ) |
| 🔁 锁复用效率 | 中:容易重复创建 Object 锁(锁粒度不够强) | 高:精确映射每个 key → ReentrantLock |
| 🔒 锁功能 | 只能用 synchronized ,不可中断,不支持 tryLock |
可选:tryLock、lockInterruptibly,灵活 |
| 🤯 错误使用风险 | 高:忘记同步、传入非唯一 key,容易失效 | 低:统一封装管理、线程安全构造 |
| 🧰 扩展能力 | 弱:不可做限时锁等高级功能 | 强:可支持监控、排队、超时等策略 |
| 🪵 日志调试能力 | 差:Object 锁难定位 | 好:每个 ReentrantLock 可打印调试信息 |
✅ 总结建议
| 适用场景 | 建议使用方案 |
|---|---|
| 🧪 简单 demo,单线程测试、小量数据 | synchronized + Map (可快速验证) |
| ⚙️ 高并发、批量 upsert、热点 key 多 | ✅ 推荐:ConcurrentMap + WeakReference<Lock> |
| 📦 希望封装通用锁池工具 | ✅ 推荐封装类:如 KeyLockManager<K> |
| 📊 需要做 tryLock / timeout / metrics | ✅ 强烈建议 ReentrantLock 方案 |
✅ 性能对比结论(大量 key 并发时)
| 场景 | 全局锁方案(Map + synchronized) | 无锁方案(ConcurrentMap + WeakReference) |
|---|---|---|
| 🔁 10万个 key,20线程并发获取锁 | ❌ 严重阻塞,吞吐低 | ✅ 基本无锁冲突,吞吐优 |
| 🔁 同一个 key 并发抢锁 | ✅ 都能互斥 | ✅ 同样互斥(更灵活) |
| 🔁 空闲 key 是否回收 | ✅ 是 | ✅ 是(更可控) |
🧩 如果你只记一句话:
🔥「全局 synchronized 是阻塞瓶颈,只适合低并发;高并发时,应使用基于 ConcurrentHashMap + WeakReference 的无锁锁池来管理 key 级锁对象。」
Q&A
为什么不用synchronized(UniqueKey.hashCode)?
synchronized (UniqueKey.hashCode) 这种方式,看起来简单粗暴 ,但实际上有几个严重问题,它并不是线程安全、稳定、可维护的解决方案。
❌ 为什么不能用 synchronized(UniqueKey.hashCode())?
1️⃣ hashCode() 只是一个 int 值,不是对象!
java
int hash = key.hashCode();
synchronized (hash) { ... } // 编译报错!
synchronized需要锁的是一个对象引用(Object),不是基本类型。hashCode()返回的是int,你不能对int加锁。
你可能会写成:
java
synchronized (Integer.valueOf(key.hashCode())) { ... }
2️⃣ 这其实是锁住了一个新对象!根本没起到锁的作用!
java
synchronized (Integer.valueOf(key.hashCode())) {
// 实际上每次都 new 了一个新的 Integer(除非值在 -128~127 缓存范围)
}
- 即使你写
Integer.valueOf(...),也几乎总是生成新的 Integer 对象(除非在[-128, 127]范围内,会被 Integer 缓存)。 - 这就意味着:每次锁的对象不同,完全无法互斥。
❌ 继续演进:尝试缓存锁对象
你可能会想:
那我是不是用一个 Map<Integer, Object> 来缓存锁对象呢?
java
Map<Integer, Object> lockMap = new ConcurrentHashMap<>();
Object lock = lockMap.computeIfAbsent(key.hashCode(), k -> new Object());
synchronized (lock) {
// do upsert
}
这也有严重问题:
3️⃣ hashCode() 不唯一!不同 key 可能 hash 相同,发生误锁
hashCode()是哈希值,不是唯一 ID。- 两个不同的
(uid, cid)可能有相同的 hashCode,但你不希望它们共享锁。 - 会导致 不同业务数据不必要地串行,性能下降,或者更严重的死锁风险(如果你用双锁)。
✅ 正确的替代方案:锁分段/对象池
我们不应该对 hashCode 本身加锁,而是使用:
✅ 分段锁机制(推荐)
java
int slotCount = 256;
ReentrantLock[] lockPool = new ReentrantLock[slotCount];
// init locks
int slot = (key.hashCode() & Integer.MAX_VALUE) % slotCount;
ReentrantLock lock = lockPool[slot];
lock.lock();
try {
// 线程安全处理
} finally {
lock.unlock();
}
优点:
- 控制锁数量,内存可控;
- 不同 key hash 到不同槽 → 并发处理;
- 同一 key 总 hash 到同一个槽 → 顺序处理;
- 性能比
**synchronized**更可控、粒度更细、死锁概率更低。
🔒 或者高级点:每个 UniqueKey 对应一个独立锁对象
这种方式可以用 LoadingCache<UniqueKey, Lock> 或 ConcurrentHashMap<UniqueKey, ReentrantLock> 来实现,但要注意:
- 要定期清理
WeakReference防止内存泄漏; - 用于超高并发热点 key 更精准控制。
✅ 总结对比
| 方法 | 是否可行 | 问题 | 备注 |
|---|---|---|---|
synchronized(hashCode()) |
❌ 不可行 | 加锁对象不是同一个 | 锁失效 |
synchronized(Integer.valueOf(hash)) |
❌ 不可行 | Integer 缓存范围有限 | 锁失效 |
Map<Integer, Object> + synchronized |
⚠️ 勉强可行 | hash 冲突严重 | 精度差,不推荐 |
| ✅ 分段锁 + ReentrantLock[] | ✅ 可行 | 轻量、性能好 | 推荐 ✅ |
| ConcurrentHashMap<UniqueKey, Lock> | ✅ 精细控制 | 内存大、需回收 | 热点业务适用 |
synchronized(String.valueOf(uid_cid)) ------ 这个方式在代码层面是可编译、可运行的,但它 看似能锁住相同的 uid+cid,却实际存在严重缺陷。我们来逐一剖析。
sychronized(String.valueOf(uid_cid))呢?
🧨 1. String.valueOf(uid_cid) 每次都是新对象!
假设你写的是:
java
String key = String.valueOf(uid + "_" + cid);
synchronized (key) {
// do upsert
}
这个 key 是一个新的 String 实例,除非你手动 intern,否则:
每次加锁的对象都不一样,根本没有加锁效果!
java
System.out.println(new String("1_2") == new String("1_2")); // false
synchronized是基于对象引用加锁的。- 如果每次创建一个新字符串,就等于每次
synchronized(new Object()),完全失效。
✅ 2. 使用 .intern() 看似可行,但问题更大!
有些人会尝试这样:
java
String key = (uid + "_" + cid).intern();
synchronized (key) {
// ...
}
这会将 key 放入 JVM 的字符串常量池,从而达到相同 uid_cid 用同一个对象加锁的目的。
但注意⚠️:
❌ 危险点:内存泄漏风险极高!!!
String.intern()会把字符串永久保存到 JVM 的常量池中(在 Java 8 之后移到了堆中,但仍不可回收,除非整个 key 无引用)。- 如果你有大量
uid+cid组合(成千上万),就会不断往字符串常量池里加对象,永久无法回收! - 严重时可能造成
**OutOfMemoryError: GC overhead limit exceeded**或**Metaspace**溢出。
🧠 所以总结:
| 方法 | 可行性 | 缺点 |
|---|---|---|
synchronized(String.valueOf(...)) |
❌ | 每次新对象,无法互斥 |
synchronized((uid + "_" + cid).intern()) |
⚠️ 有效但高风险 | 内存泄漏 |
| ✅ 分段锁(ReentrantLock[]) | ✅ 推荐 | 性能好、无泄漏 |
✅ ConcurrentHashMap<UniqueKey, Lock> + WeakReference |
✅ 高级 | 控制好内存,可精准锁 |
🔚 总结一句话:
**synchronized(String.valueOf(...))**** 是"看起来能锁,实际不会锁"的经典陷阱。用 ReentrantLock + 分段锁才是靠谱的并发控制方式。**