在Java里如何减少锁竞争_Java并发性能优化思路解析

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) 防止线程无限等待

改用线程安全但无锁的数据结构

很多锁竞争其实源于对 HashMapArrayList 等非线程安全容器的“手动加锁封装”。与其自己锁住整个结构,不如直接选用设计上就规避锁竞争的替代品。

例如:ConcurrentHashMap 在 JDK 8+ 中已完全摒弃分段锁(Segment),改用 synchronized + CAS 控制桶级锁;LongAdder 对高频计数场景比 AtomicLong 更低竞争——它内部用 cell 数组分散写压力,只有在调用 sum() 时才合并。

  • ConcurrentHashMapcomputeIfAbsent() 是原子的,别再外面套 synchronized
  • 高并发累加场景优先选 LongAdder,而非 AtomicLong.incrementAndGet()
  • CopyOnWriteArrayList 适合读多写少,但写操作会复制整个数组,别在写频繁场景误用

避免共享状态:用 ThreadLocal 或不可变对象隔离数据

锁的本质是协调对共享可变状态的访问。如果能把状态从“共享”变成“线程私有”,锁自然就消失了。

ThreadLocal 是最常用的手段,尤其适合保存上下文信息(如用户 ID、事务 ID、格式化器)。但要注意内存泄漏风险:在线程池场景下,必须显式调用

remove() 清理,否则 ThreadLocalMap 中的 Entry 会持有 ThreadLocal 的强引用,导致无法回收。

  • new ThreadLocal() 后,务必在业务逻辑结束前调用 threadLocal.remove()
  • 优先用 final 类型字段 + 构造参数初始化对象,减少运行时状态变更
  • LocalDateTimeString 这类不可变类,天然线程安全,别轻易包装成可变容器

慎用 synchronized(this) 和 public 锁对象

这是最容易引发意外锁竞争的地方。synchronized(this) 暴露了实例锁给外部代码,别人只要拿到你的对象引用,就能调用 wait()notify() 或直接加锁阻塞你;而 public final Object lock = new Object() 同样危险——外部类可能无意中用它做同步,导致锁行为不可控。

正确做法是声明私有锁对象,并限制其作用域:

private final Object writeLock = new Object();
// 外部无法访问 writeLock,只能通过你暴露的线程安全方法操作
  • 永远不要用 thisgetClass() 或公共字段作为锁对象
  • 若需类级别同步,用 private static final Object CLASS_LOCK = new Object()
  • 锁对象类型推荐 Object,避免用 String 或装箱类型(存在常量池/缓存复用风险)
锁竞争优化不是堆砌工具,而是持续识别“谁在争什么资源”。最容易被忽略的是:你以为的热点路径,可能只是日志打印或监控埋点触发的伪竞争;而真正卡住系统的,反而是某个被反复调用却没加缓存的配置读取。先用 jstackArthas thread 确认竞争点,再动手改。