# 如何优化 Java 中的锁的使用?
6 min read
Table of Contents
如何优化 Java 中的锁的使用?
如何优化 Java 中的锁的使用?
【核心定义】
优化 Java 锁使用的本质是通过减少锁的粒度、降低锁的持有时间、避免不必要的锁竞争,以及选择合适的并发工具,来提升多线程程序的吞吐量和响应性,同时保证线程安全。
【关键要点】
- 减小锁的粒度:将一个大锁拆分为多个小锁,减少锁的竞争范围。例如,ConcurrentHashMap 使用分段锁(JDK 7)或 CAS + synchronized(JDK 8)来替代 Hashtable 的全局锁。
- 缩短锁的持有时间:只在必须同步的代码块上加锁,尽快释放锁。优先使用
synchronized块而非synchronized方法。 - 使用读写锁(ReadWriteLock):区分读操作和写操作,允许多个线程同时读,写操作独占锁。适用于读多写少的场景,如
ReentrantReadWriteLock。 - 使用乐观锁与 CAS:对于低竞争场景,使用原子类(如
AtomicInteger)基于 CAS(Compare-And-Swap)实现无锁编程,避免线程阻塞。 - 锁分离与锁粗化:
- 锁分离:将锁的功能分离,如 LinkedBlockingQueue 使用 putLock 和 takeLock 分别控制插入和移除。
- 锁粗化:在频繁加锁/解锁的循环中,将锁范围扩大到循环外部,减少锁开销(JVM 会自动优化)。
- 使用并发容器:直接使用
java.util.concurrent包中的线程安全容器(如ConcurrentHashMap、CopyOnWriteArrayList),避免手动加锁。 - 避免死锁:按固定顺序获取锁、使用超时机制(如
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:分段锁或桶锁,高并发下性能优异。
『面试官追问』
- synchronized 和 ReentrantLock 在性能上有什么区别?
- JDK 6 后
synchronized经过优化(偏向锁、轻量级锁),性能与ReentrantLock接近,默认场景推荐synchronized;需要高级功能(如公平锁)时用ReentrantLock。
- JDK 6 后
- CAS 的 ABA 问题如何解决?
- 使用版本号或时间戳,如
AtomicStampedReference。
- 使用版本号或时间戳,如
- 什么是锁消除和锁粗化?JVM 如何做的?
- 锁消除:JIT 编译时,如果发现锁对象不可能被共享(如局部变量),则移除锁。
- 锁粗化:将相邻的多个锁操作合并为一个,减少锁开销。
- 如何诊断死锁?
- 使用
jstack查看线程转储,或 JDK 自带的jconsole/VisualVM检测死锁。
- 使用
- 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提供更底层的内存操作。
【总结】
优化锁使用的核心思路是:减少竞争、缩短时间、选择工具。实践中需结合场景分析,优先使用高层并发工具,必要时进行细粒度调优,并通过压测验证。