一句话
synchronized早就不是"重量级锁"了。JDK6 引入了锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),大部分情况下只到轻量级锁(自旋),性能和ReentrantLock差不多。95% 场景用 synchronized 就够了。
synchronized 锁升级过程
无锁状态 ↓ 第一个线程来竞争 偏向锁(只记录线程 ID,CAS 一次搞定) ↓ 第二个线程来竞争 轻量级锁(自旋等待,不阻塞) ↓ 自旋超过一定次数 重量级锁(OS 互斥锁,线程阻塞/唤醒)偏向锁
适用场景:只有一个线程反复加同一把锁(比如StringBuffer只被一个线程操作)。
原理: JVM 在对象头里记录当前持有锁的线程 ID 下次同一个线程再来加锁 → 检查 ID 一致 → 直接通过(零开销) 相当于"VIP 通道",因为大部分锁实际上只有一个线程在用JDK15 开始默认关闭偏向锁(
-XX:-UseBiasedLocking),因为现代并发场景下偏向锁维护成本 > 收益。面试提到这个加分。
轻量级锁
适用场景:两个线程交替使用同一把锁,竞争不激烈。
原理: 在线程栈帧里创建 Lock Record,CAS 把对象头指向 Lock Record 另一个线程发现锁被占了 → 不阻塞,而是自旋(空转等着) 像打电话占线 → 不挂断,每隔几秒重拨一次 自旋一定次数后还拿不到 → 升级为重量级锁重量级锁
适用场景:多线程激烈竞争。
原理: 依赖操作系统层面的互斥锁(mutex) 线程拿不到锁 → 进入阻塞状态 → 涉及用户态↔内核态切换 这个切换非常昂贵(微秒级)为什么说 synchronized "不重"了?
锁升级的核心思路:用最轻的方式解决问题 只有一个线程 → 偏向锁(几乎零开销) 两个线程交替 → 轻量级锁(自旋,比阻塞轻得多) 真正激烈竞争 → 重量级锁(最后手段) 大部分实际场景停留在轻量级锁阶段,根本到不了重量级锁。 所以" synchronized 太重"这个说法在 JDK6 之后就不成立了。synchronized vs ReentrantLock
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 本质 | JVM 内置关键字 | java.util.concurrent 包的类(基于 AQS) |
| 锁释放 | 自动(出作用域释放) | 必须手动unlock()(finally 中) |
| 可中断 | 不行,等锁时只能傻等 | lockInterruptibly()可响应中断 |
| 公平锁 | 只支持非公平 | 支持公平 / 非公平 |
| 条件变量 | 只有一组wait()/notify() | 多个Condition(await/signal) |
| 尝试加锁 | 不支持 | tryLock(timeout)超时放弃 |
| 锁升级 | 有(偏向 → 轻量 → 重量) | 无此机制 |
| 性能 | JDK6+ 差距很小 | 高竞争下略优 |
ReentrantLock 底层:AQS
ReentrantLock 内部继承 AQS(AbstractQueuedSynchronizer) AQS 核心设计: state 变量:int,0=未锁定,>0=锁定次数(ReentrantLock 可重入) CLH 双向队列:排队的线程链表 加锁流程: ① CAS 尝试把 state 从 0 改为 1 ② 成功 → 拿到锁 ③ 失败 → 当前线程封装成 Node 入 CLH 队列 → 阻塞等待 ④ 前一个线程释放锁 → unpark 队列中下一个线程 → 唤醒 公平 vs 非公平: 非公平锁(默认):新线程先 CAS 抢,抢不到再排队 → 吞吐量更高 公平锁:严格按 CLH 队列顺序来 → 不饥饿但吞吐量低面试追问:“AQS 除了 ReentrantLock 还被谁用了?” →
Semaphore(信号量)、CountDownLatch、ReentrantReadWriteLock都基于 AQS。
标准写法对比
synchronized
// 简洁,自动释放,不会忘publicsynchronizedvoidtransfer(Accounttarget,intamount){this.balance-=amount;target.balance+=amount;}ReentrantLock
// 必须手动释放,忘记就死锁privatefinalReentrantLocklock=newReentrantLock();publicvoidtransfer(Accounttarget,intamount){lock.lock();try{this.balance-=amount;target.balance+=amount;}finally{lock.unlock();// 必须在 finally 里}}🎙 面试回答模板
"synchronized 和 ReentrantLock 的区别,我从五个维度来说: 第一,锁释放方式不同: synchronized 是 JVM 关键字,出作用域自动释放; ReentrantLock 是 API 层面的,必须手动在 finally 中 unlock。 第二,可中断性不同: synchronized 等锁时不能中断,只能一直等; ReentrantLock 可以用 lockInterruptibly 响应中断。 第三,条件变量不同: synchronized 只有一组 wait/notify; ReentrantLock 支持多个 Condition,可以做更精细的线程通信。 第四,锁的公平性: synchronized 只支持非公平锁; ReentrantLock 可以选择公平锁或非公平锁。 第五,尝试加锁: synchronized 不支持,ReentrantLock 可以 tryLock 设置超时。 不过有一点要纠正——synchronized 不是重量级锁。 JDK6 之后有锁升级机制:偏向锁 → 轻量级锁 → 重量级锁, 大部分情况只到轻量级锁,性能差距很小。 所以 95% 场景用 synchronized 就够了, 需要灵活控制中断、超时、多条件时才用 ReentrantLock。"参考来源
- 《Java 并发编程的艺术》第 2 章
- JDK8
java.util.concurrent.locks.ReentrantLock源码 - JDK8
AbstractQueuedSynchronizer源码