# 如何优化 Java 中的锁的使用?

6 min read
Table of Contents

如何优化 Java 中的锁的使用?

如何优化 Java 中的锁的使用?

【核心定义】

优化 Java 锁使用的本质是通过减少锁的粒度、降低锁的持有时间、避免不必要的锁竞争,以及选择合适的并发工具,来提升多线程程序的吞吐量和响应性,同时保证线程安全。

【关键要点】

  1. 减小锁的粒度:将一个大锁拆分为多个小锁,减少锁的竞争范围。例如,ConcurrentHashMap 使用分段锁(JDK 7)或 CAS + synchronized(JDK 8)来替代 Hashtable 的全局锁。
  2. 缩短锁的持有时间:只在必须同步的代码块上加锁,尽快释放锁。优先使用 synchronized 块而非 synchronized 方法。
  3. 使用读写锁(ReadWriteLock):区分读操作和写操作,允许多个线程同时读,写操作独占锁。适用于读多写少的场景,如 ReentrantReadWriteLock。
  4. 使用乐观锁与 CAS:对于低竞争场景,使用原子类(如 AtomicInteger)基于 CAS(Compare-And-Swap)实现无锁编程,避免线程阻塞。
  5. 锁分离与锁粗化:
    • 锁分离:将锁的功能分离,如 LinkedBlockingQueue 使用 putLock 和 takeLock 分别控制插入和移除。
    • 锁粗化:在频繁加锁/解锁的循环中,将锁范围扩大到循环外部,减少锁开销(JVM 会自动优化)。
  6. 使用并发容器:直接使用 java.util.concurrent 包中的线程安全容器(如 ConcurrentHashMap、CopyOnWriteArrayList),避免手动加锁。
  7. 避免死锁:按固定顺序获取锁、使用超时机制(如 tryLock)、减少嵌套锁。

【深度推导/细节】

锁优化的核心矛盾:性能 vs. 线程安全

  • 性能瓶颈:锁的竞争会导致线程阻塞、上下文切换,降低 CPU 利用率。
  • 解决方案逻辑拆解:
    • Step 1: 分析竞争热点:使用 Profiler 工具(如 JProfiler)定位高竞争锁。
    • Step 2: 评估读写比例:读多写少 → 读写锁;写多或竞争高 → 考虑 CAS 或细粒度锁。
    • Step 3: 选择锁策略:
      • 低竞争、简单操作 → CAS(原子类)。
      • 中等竞争、复杂操作 → synchronized 或 ReentrantLock。
      • 高竞争、数据结构复杂 → 并发容器或自定义分段锁。
    • Step 4: 实施与测试:在保证线程安全前提下,通过基准测试(JMH)验证优化效果。

关键数字与设计合理性

  • 锁膨胀:synchronized 锁会从偏向锁 → 轻量级锁 → 重量级锁升级,适应不同竞争强度。
  • CAS 自旋次数:默认 10 次(可通过 -XX:PreBlockSpin 调整),超过后升级为重量级锁,平衡自旋开销与阻塞开销。
  • ConcurrentHashMap 并发度:JDK 7 中分段锁的段数默认为 16,减少竞争;JDK 8 中改为 CAS + synchronized 锁单个桶(链表头/红黑树根)。

【关联/对比】

  • synchronized vs. ReentrantLock:
    • synchronized:JVM 内置锁,自动释放,支持锁升级,不可中断。
    • ReentrantLock:API 级别锁,需手动释放,支持公平锁、可中断、超时、条件变量。
  • 乐观锁 vs. 悲观锁:
    • 乐观锁:CAS,假设冲突少,适合读多写少。
    • 悲观锁:synchronized/ReentrantLock,假设冲突多,适合写多。
  • ConcurrentHashMap vs. Hashtable:
    • Hashtable:全局锁,性能差。
    • ConcurrentHashMap:分段锁或桶锁,高并发下性能优异。

『面试官追问』

  1. synchronized 和 ReentrantLock 在性能上有什么区别?
    • JDK 6 后 synchronized 经过优化(偏向锁、轻量级锁),性能与 ReentrantLock 接近,默认场景推荐 synchronized;需要高级功能(如公平锁)时用 ReentrantLock。
  2. CAS 的 ABA 问题如何解决?
    • 使用版本号或时间戳,如 AtomicStampedReference。
  3. 什么是锁消除和锁粗化?JVM 如何做的?
    • 锁消除:JIT 编译时,如果发现锁对象不可能被共享(如局部变量),则移除锁。
    • 锁粗化:将相邻的多个锁操作合并为一个,减少锁开销。
  4. 如何诊断死锁?
    • 使用 jstack 查看线程转储,或 JDK 自带的 jconsole/VisualVM 检测死锁。
  5. ConcurrentHashMap 在 JDK 7 和 JDK 8 中实现有何不同?
    • JDK 7:分段锁(Segment),每段一个锁。
    • JDK 8:数组 + 链表/红黑树,使用 CAS 插入头节点,synchronized 锁链表头或树根,粒度更细。

【版本差异】

  • JDK 5:引入 java.util.concurrent,提供 ReentrantLock、原子类、并发容器。
  • JDK 6:对 synchronized 进行优化(偏向锁、轻量级锁、适应性自旋)。
  • JDK 7:改进并发工具,如 ForkJoinPool。
  • JDK 8:ConcurrentHashMap 改用 CAS + synchronized,提升并发度;引入 StampedLock(乐观读锁)。
  • JDK 9+:持续优化并发性能,如 VarHandle 提供更底层的内存操作。

【总结】

优化锁使用的核心思路是:减少竞争、缩短时间、选择工具。实践中需结合场景分析,优先使用高层并发工具,必要时进行细粒度调优,并通过压测验证。

My avatar

文章看完了,不打算去别的篇章转转吗?👀 感谢你的耐心阅读!如果你有问题想探讨,或者发现了文中的 Bug,请务必通过页脚的链接联系我


More Posts

# 如何在 git clone 时指定分支?

4 min read

【核心定义】在 git clone 时指定分支是通过 -b 参数或 --branch 选项,配合远程仓库的分支名,实现克隆时仅拉取指定分支的最新提交,并自动设置本地分支与远程分支的追踪关系。

Read

Comments