在Java多线程的世界里,锁是守护数据安全的“门神”。但面对synchronized和ReentrantLock这两位门神,开发者常陷入选择困难:一个像全自动智能锁,另一个像可编程电子锁。今天,我们就来拆解它们的“内核”,看看谁更适合你的高并发战场。
出身背景:JVM亲儿子 vs 后天努力的优等生
- synchronized:根正苗红的语言级锁
- 身份:Java关键字,由JVM直接支持,代码简洁到只需一个单词。
- 底层原理:依赖对象头的Mark Word和Monitor(监视器),通过monitorenter和monitorexit字节码指令实现锁的获取与释放。
- 锁升级:从偏向锁(无竞争)、轻量级锁(短时间竞争)到重量级锁(激烈竞争),动态适应场景以减少开销。
- ReentrantLock:API界的锁王
- 身份:JDK 1.5引入的类库锁,需手动调用lock()和unlock()。
- 底层原理:基于AbstractQueuedSynchronizer(AQS),通过CAS(Compare And Swap)和自旋机制实现非阻塞竞争。
- 设计哲学:将锁的控制权交给开发者,提供更多“自定义选项”。
对比小结:
- synchronized是“傻瓜式”解决方案,适合快速开发;
- ReentrantLock是“乐高式”工具包,适合需要精细控制的场景。
功能对比:基础功能 vs 豪华套餐
- 锁的公平性
- synchronized:非公平锁,新来的线程可能插队,导致“饿死”老线程。
- ReentrantLock:可切换公平模式(new ReentrantLock(true)),按请求顺序分配锁,但吞吐量可能下降20%-30%。
- 中断响应
synchronized:无法中断,线程一旦阻塞就只能死等,容易引发死锁僵局。
ReentrantLock:支持lockInterruptibly(),允许外部强制中断等待,例如:
try { lock.lockInterruptibly(); // 业务代码 } catch (InterruptedException e) { // 处理中断(比如回滚事务) } 这一特性在分布式锁超时场景中堪称“救命稻草”。
- 超时机制
- synchronized:无超时设置,只能被动等待。
- ReentrantLock:通过tryLock(long timeout, TimeUnit unit)实现“限时抢锁”,避免无限期阻塞。
- 条件变量(Condition)
synchronized:依赖wait()/notify(),只能绑定一个等待队列,唤醒操作是“随机广播”。
ReentrantLock:支持创建多个Condition对象,实现精准唤醒。例如生产者-消费者模型中,可以分别控制“非满”和“非空”条件:
Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition(); // 生产者等待notFull,消费者唤醒notEmpty 功能评分:
- synchronized:★★★(满足基础需求)
- ReentrantLock:★★★★★(功能全面,适合复杂业务)
性能之争:优化后的逆袭 vs 灵活的代价
- 低竞争场景
- synchronized:偏向锁模式下几乎零开销(直接标记线程ID),性能优于ReentrantLock。
- ReentrantLock:即使无竞争,仍需走CAS流程,存在轻微性能损耗。
- 高并发场景
- synchronized:升级为重量级锁后,线程会进入内核态阻塞,上下文切换成本高。
- ReentrantLock:通过自旋尝试避免阻塞,尤其在非公平模式下,吞吐量更高。
- Java版本的影响
JDK 1.6后,synchronized引入锁消除(Lock Elision)和锁粗化(Lock Coarsening),进一步缩小与ReentrantLock的差距。
性能建议:
- 80%的日常场景中,两者差异可忽略,优先选synchronized;
- 秒杀、高频交易等极端场景,ReentrantLock的非阻塞特性更具优势。
开发体验:简洁 vs 灵活
- 代码复杂度
synchronized:一行代码解决问题,但可能因锁范围过大导致性能问题。
ReentrantLock:需手动管理锁,忘记unlock()会导致灾难性后果,必须配合try-finally使用:
lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } - 调试与监控
- synchronized:JVM内置支持,可通过jstack查看锁状态(如BLOCKED线程)。
- ReentrantLock:提供getHoldCount()、isLocked()等API,便于监控锁的持有情况。
开发建议:
- 新手或简单业务优先用synchronized,减少出错概率;
- 复杂系统或需要精细化控制时,切换ReentrantLock。
实战案例:死锁逃生指南
场景:线程A持有锁1请求锁2,线程B持有锁2请求锁1,形成死锁。
- synchronized的困境
无解!只能重启服务或等待线程假死。
- ReentrantLock的破局
通过lockInterruptibly()+Thread.interrupt(),强制中断一方并释放锁:
// 线程A try { lock1.lockInterruptibly(); Thread.sleep(100); lock2.lockInterruptibly(); } catch (InterruptedException e) { System.out.println("线程A放弃,释放锁1"); lock1.unlock(); } 结论:在高风险场景中,ReentrantLock的“逃生通道”设计更可靠。
选型决策树:你的业务适合哪种锁?
- 优先选synchronized的情况
锁竞争不激烈(如单例模式);
需要快速实现基础同步;
团队对并发控制经验有限。
- 必须选ReentrantLock的情况
- 需要公平锁(如票务系统按顺序分配资源);
- 要求锁超时或可中断(如支付系统的防死锁设计);
- 使用条件变量实现复杂协作(如多条件生产者消费者)。
没有最好的锁,只有最合适的锁
synchronized像自动驾驶汽车——简单安全,适合日常通勤;ReentrantLock像手动挡跑车——操控精准,适合赛道竞速。理解它们的差异,才能在并发编程的征途中游刃有余。