ReentrantLock 适合细粒度锁控制,需使锁范围与共享状态修改边界严格对齐;优先用 lock.lock()/unlock() 配 try-finally,避免耗时操作和死锁;可考虑无锁数据结构、ThreadLocal、不可变对象及私有锁对象优化竞争。

用 ReentrantLock 替代 synchronized 做细粒度控制
不是所有场景都适合直接换锁,但当你发现 synchronized 方法或代码块锁住的范围远超实际需要时,ReentrantLock 提供了更灵活的加锁边界。比如一个集合类里只有写操作需要互斥,读操作完全可以无锁并发执行——这时把锁从方法级别下沉到具体修改集合的几行代码里,能显著降低竞争概率。
关键点在于:锁的粒度要和「共享状态的修改边界」严格对齐。常见错误是把整个业务逻辑包进一个锁,而其实只有 counter++ 或 map.put(key, value) 这类操作真正需要保护。
- 优先使用
lock.lock()/lock.unlock()配合try-finally,避免死锁 - 不要在
lock()和unlock()之间做耗时操作(如 I/O、远程调用) - 考虑用
lock.tryLock(timeout, TimeUnit)防止线程无限等待
改用线程安全但无锁的数据结构
很多锁竞争其实源于对 HashMap、ArrayList 等非线程安全容器的“手动加锁封装”。与其自己锁住整个结构,不如直接选用设计上就规避锁竞争的替代品。
例如:ConcurrentHashMap 在 JDK 8+ 中已完全摒弃分段锁(Segment),改用 synchronized + CAS 控制桶级锁;LongAdder 对高频计数场景比 AtomicLong 更低竞争——它内部用 cell 数组分散写压力,只有在调用 sum() 时才合并。
立即学习“Java免费学习笔记(深入)”;
-
ConcurrentHashMap的computeIfAbsent()是原子的,别再外面套synchronized - 高并发累加场景优先选
LongAdder,而非AtomicLong.incrementAndGet() -
CopyOnWriteArrayList适合读多写少,但写操作会复制整个数组,别在写频繁场景误用
避免共享状态:用 ThreadLocal 或不可变对象隔离数据
锁的本质是协调对共享可变状态的访问。如果能把状态从“共享”变成“线程私有”,锁自然就消失了。
ThreadLocal 是最常用的手段,尤其适合保存上下文信息(如用户 ID、事务 ID、格式化器)。但要注意内存泄漏风险:在线程池场景下,必须显式调用 remove() 清理,否则 ThreadLocalMap 中的 Entry 会持有 ThreadLocal 的强引用,导致无法回收。
- 用
new ThreadLocal()后,务必在业务逻辑结束前调用threadLocal.remove() - 优先用
final类型字段 + 构造参数初始化对象,减少运行时状态变更 - 像
LocalDateTime、String这类不可变类,天然线程安全,别轻易包装成可变容器
慎用 synchronized(this) 和 public 锁对象
这是最容易引发意外锁竞争的地方。synchronized(this) 暴露了实例锁给外部代码,别人只要拿到你的对象引用,就能调用 wait()、notify() 或直接加锁阻塞你;而 public final Object lock = new Object() 同样危险——外部类可能无意中用它做同步,导致锁行为不可控。
private final Object writeLock = new Object(); // 外部无法访问 writeLock,只能通过你暴露的线程安全方法操作
- 永远不要用
this、getClass()或公共字段作为锁对象 - 若需类级别同步,用
private static final Object CLASS_LOCK = new Object() - 锁对象类型推荐
Object,避免用String或装箱类型(存在常量池/缓存复用风险)
jstack 或 Arthas thread 确认竞争点,再动手改。











