1. 为什么我们需要Lock锁
在Java并发编程的世界里,锁机制就像十字路口的交通信号灯。想象一下早高峰时没有红绿灯的十字路口会是什么场景——这就是多线程环境下没有同步机制的程序状态。synchronized关键字作为Java原生的同步工具,就像基础款的红绿灯,而Lock接口及其实现类则像是配备了智能感应和优先通行功能的现代化交通控制系统。
我清楚地记得第一次处理银行转账死锁问题的经历。两个线程互相持有对方需要的锁,整个系统卡死的样子,就像两辆互不相让的卡车把路口堵得水泄不通。这时我才真正理解Java并发包作者Doug Lea设计Lock接口的良苦用心——我们需要比synchronized更灵活、更强大的并发控制工具。
2. Lock锁核心实现解析
2.1 锁的三大金刚
ReentrantLock是最常用的锁实现,它的可重入特性就像酒店房卡——同一个线程可以多次获取同一把锁而不会把自己锁在外面。底层通过AQS(AbstractQueuedSynchronizer)实现,这个并发包的核心组件堪称Java并发编程的"瑞士军刀"。
Lock lock = new ReentrantLock(); lock.lock(); try { // 临界区代码 } finally { lock.unlock(); // 必须放在finally块 }重要提示:忘记unlock就像忘记关水龙头,会导致资源泄漏。我建议在IDE中设置代码模板,确保每次lock()后都自动生成对应的unlock()。
ReadWriteLock将锁细分为读锁和写锁,就像图书馆的管理规则——多个读者可以同时阅读,但写者需要独占访问。这种设计在读多写少的场景下性能提升显著:
ReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); Lock writeLock = rwLock.writeLock();StampedLock是Java 8引入的优化版读写锁,加入了乐观读模式。它就像超市的快速通道——如果没遇到修改,乐观读可以直接通过而不需要完全加锁。
2.2 AQS工作原理揭秘
AQS内部维护着一个CLH队列(Craig, Landin, and Hagersten lock queue),这个虚拟队列就像医院挂号系统:
- 每个等待线程都是一个"挂号单"
- 通过CAS操作保证线程安全
- 采用"自旋+CAS+park"的混合策略
我曾在性能调优时用jstack抓到过这样的线程堆栈:
"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f487c0ae800 nid=0x1e03 waiting on condition [0x00007f487aefd000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000076c182e08> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)这就是典型的锁等待场景。理解这些底层机制,才能在出现性能问题时准确诊断。
3. 高级锁使用技巧
3.1 条件变量的妙用
Condition接口实现了管程模型的等待/通知机制。我在实现一个有限容量队列时,是这样使用它的:
public class BoundedQueue<T> { private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public void put(T item) throws InterruptedException { lock.lock(); try { while (queue.isFull()) { notFull.await(); // 释放锁并等待 } queue.enqueue(item); notEmpty.signal(); } finally { lock.unlock(); } } }这种模式比Object.wait()/notify()更灵活,可以创建多个等待条件。就像餐厅里可以分别设置"等位区"和"等餐区"。
3.2 锁性能优化实战
通过JMH基准测试,我发现这些优化策略效果显著:
| 策略 | 吞吐量提升 | 适用场景 |
|---|---|---|
| 减小锁粒度 | 40%-60% | 大对象分解 |
| 锁分离 | 70%+ | 读写分离 |
| 锁粗化 | 10%-20% | 连续小操作 |
| 无锁结构 | 2-3倍 | 计数器等 |
特别提醒:不要盲目使用"锁消除"优化。我曾见过一个案例,因为误用ThreadLocal导致锁消除,结果在多线程环境下出现数据错乱。
4. 锁的陷阱与规避方案
4.1 死锁预防四法则
- 顺序加锁:就像餐厅要求所有人都必须先拿叉子再拿刀子
- 超时机制:tryLock(timeout)比lock()更安全
- 开放调用:不在持有锁时调用外部方法
- 锁检测:定期用jstack或VisualVM检查
这是我常用的死锁检测代码:
ThreadMXBean bean = ManagementFactory.getThreadMXBean(); long[] threadIds = bean.findDeadlockedThreads(); if (threadIds != null) { ThreadInfo[] infos = bean.getThreadInfo(threadIds); for (ThreadInfo info : infos) { System.err.println("死锁 detected: " + info); } }4.2 锁性能问题诊断
当系统出现吞吐量下降时,我通常会这样排查:
- 用
jstack -l pid查看锁竞争情况 - 使用Arthas的
monitor命令统计方法调用 - 通过JFR(Java Flight Recorder)分析锁持有时间
常见反模式:
- 在循环内加锁
- 锁中执行IO操作
- 锁嵌套层级过深
5. 现代并发工具的选择
虽然Lock强大,但新时代的并发需求催生了更多选择:
并发容器:ConcurrentHashMap的锁分段技术就像把一个大仓库分成多个小隔间
原子变量:AtomicInteger等采用CAS实现,适合计数器场景
Fork/Join框架:分治思想的并发实现,适合计算密集型任务
CompletableFuture:异步编程的利器,避免显式锁的使用
在我最近的一个电商项目中,最终采用了这样的混合方案:
- 库存扣减:Redis分布式锁
- 订单处理:数据库乐观锁
- 统计计算:LongAdder
- 缓存更新:StampedLock
这种根据场景选择最合适工具的思维方式,往往比单纯追求技术先进性更重要。就像木匠不会只用锤子处理所有工作,成熟的开发者应该构建自己的并发工具箱。