一、先说结论
面试时被问到这个问题,直接先甩结论:
| 特性 | synchronized | JUC Lock(ReentrantLock) |
|---|---|---|
| 实现层级 | JVM 内置(C++) | Java + AQS 框架 |
| 锁释放 | 自动释放 | 手动释放(必须 finally) |
| 公平锁 | ❌ 不支持 | ✅ 支持 |
| 可中断等待 | ❌ 不支持 | ✅ 支持(lockInterruptibly) |
| 多条件队列 | ❌ 单个 wait/notify | ✅ 多个 Condition |
| 锁尝试获取 | ❌ 不支持 | ✅ 支持(tryLock) |
| 性能(JDK 1.6+) | 几乎一致 | 高并发略优 |
记住一句话:synchronized 能用就用,需要高级特性再换 Lock。
二、synchronized 的底层原理
很多人只知道 synchronized 能加锁,但不知道它背后发生了什么。
2.1 锁存在哪?—— Mark Word
Java 对象的内存分为三部分:对象头、实例数据、对齐填充。
synchronized 的锁信息存在对象头的 Mark Word 里。
32 位 JVM 的 Mark Word 结构:
| 状态 | Mark Word 内容 |
|---|---|
| 无锁 | hashcode(25位) + age(4位) + biased_lock(1位) + lock(2位) |
| 偏向锁 | thread(23位) + epoch(2位) + age(4位) + biased_lock(1位) + lock(2位) |
| 轻量级锁 | 指向栈中锁记录的指针(30位) |
| 重量级锁 | 指向 Monitor 的指针(30位) |
64 位 JVM 类似,只是位数扩展了。
2.2 锁升级过程
这是面试超高频题,必须搞清楚。
无锁 → 偏向锁 → 轻量级锁 → 重量级锁阶段一:偏向锁
第一个线程访问同步块时,JVM 把线程 ID 记录到 Mark Word。
Mark Word: [线程ID | 偏向锁标志位 | lock标志位]下次这个线程再进入,只需要比较线程 ID,零 CAS 开销。
阶段二:轻量级锁
第二个线程来了,发现 Mark Word 里的偏向锁不是自己的。
JVM 在当前线程的栈帧里创建一个锁记录(Lock Record),然后 CAS 把 Mark Word 复制到栈帧,同时把 Mark Word 改成指向栈帧的指针。
Mark Word: [指向栈帧Lock Record的指针]其他线程来了就自旋等着,不阻塞线程,只是空转 CPU。
阶段三:重量级锁
自旋次数超过阈值(默认 10 次),或者自旋线程太多(CPU 核数一半以上),锁膨胀为重量级锁。
重量级锁依赖操作系统的 Mutex 实现,线程进入阻塞状态,不消耗 CPU。
Mark Word: [指向Monitor的指针] Monitor: Owner(持有者) + EntryList(等待队列) + WaitSet(等待notify的队列)这就是为什么很多人说 synchronized “慢”,因为它早期确实是重量级锁。但 JDK 1.6 之后加了偏向锁和轻量级锁优化,日常场景基本和 ReentrantLock 性能持平。
踩坑一:偏向锁在 JDK 15 后被废弃
如果你用的是 JDK 15+ 的版本,默认已经禁用了偏向锁(-XX:+UseBiasedLocking默认 false)。
线上有同学碰到过,因为这个参数导致 synchronized 性能不符合预期。
三、ReentrantLock 的底层原理
ReentrantLock 是 JUC 里最常用的锁,它的底层靠的是AQS(AbstractQueuedSynchronizer)。
3.1 AQS 是什么?
AQS 是 JUC 包里很多组件的基石,不只是锁:
ReentrantLock → AQS Semaphore → AQS CountDownLatch → AQS CyclicBarrier → ReentrantLock ...AQS 核心结构就两个东西:
state:一个 volatile 的 int,表示锁状态
0:没有被持有
0:被持有,值 = 重入次数
CLH 队列:一个双向链表,存储等待线程
3.2 加锁流程(以非公平锁为例)
// ReentrantLock.NonfairSync.lock()finalvoidlock(){// 1. 直接 CAS 抢锁if(compareAndSetState(0,1))setExclusiveOwnerThread(Thread.currentThread());else// 2. 抢不到,尝试获取锁acquire(1);}// AQS.acquire()publicfinalvoidacquire(intarg){// 3. tryAcquire 由子类实现(ReentrantLock重写)if(!tryAcquire(arg)&&// 4. 获取失败,加入等待队列acquireQueued(addWaiter(Node.EXCLUSIVE),arg))// 5. 如果被中断过,自旋中止selfInterrupt();}关键点:非公平锁上线就 CAS 抢,不排队。公平锁会先看看队列里有没有人,有的话就老实排队。
3.3 解锁流程
publicvoidunlock(){sync.release(1);}// AQS.release()publicfinalbooleanrelease(intarg){if(tryRelease(arg)){Nodeh=head;if(h!=null&&h.waitStatus!=0)// 唤醒后继节点unparkSuccessor(h);returntrue;}returnfalse;}ReentrantLock 的可重入:每加一次锁 state +1,每解锁一次 state -1,必须完全释放(state 归 0)才真正解锁。
四、核心区别对比
4.1 功能维度
| 能力 | synchronized | ReentrantLock |
|---|---|---|
| 锁获取 | synchronized void method() | lock.lock() |
| 锁释放 | 自动(出方法体) | 手动lock.unlock() |
| 尝试获取 | ❌ | ✅lock.tryLock() |
| 超时获取 | ❌ | ✅lock.tryLock(5, TimeUnit.SECONDS) |
| 可中断等待 | ❌ | ✅lock.lockInterruptibly() |
| 公平锁 | ❌ | ✅new ReentrantLock(true) |
| 多条件控制 | ❌(单 wait 队列) | ✅(多个 Condition) |
4.2 代码写法对比
synchronized 版:
synchronized(this){count++;}// 自动释放,不用管ReentrantLock 版:
privatefinalReentrantLocklock=newReentrantLock();publicvoidincrement(){lock.lock();try{count++;}finally{lock.unlock();// 必须手动释放,而且要放 finally!}}这就是为什么 synchronized 更安全——忘了写 finally 块,ReentrantLock 的锁永远不会释放,程序分分钟死锁。
4.3 条件变量的坑
synchronized 的 wait/notify 只能有一个等待队列。
ReentrantLock 的 Condition 可以创建多个独立队列:
ReentrantLocklock=newReentrantLock();ConditionconditionA=lock.newCondition();ConditionconditionB=lock.newCondition();// 线程 A 等待 conditionAconditionA.await();// 线程 B 等待 conditionBconditionB.await();// 唤醒 conditionA 上的所有线程conditionA.signalAll();典型场景:生产者-消费者,一个队列放生产者,一个队列放消费者,比 synchronized 的 wait/notify 清晰得多。
踩坑二:ReentrantLock 的手动释放陷阱
publicvoidwrong(){lock.lock();if(condition){return;// ❌ 锁永远不释放!}lock.unlock();}publicvoidright(){lock.lock();try{if(condition){return;// ✅ try 块里随便 return}}finally{lock.unlock();// finally 保证释放}}五、ReadWriteLock:读多写少场景的利器
synchronized 和 ReentrantLock 都是排他锁,同一时刻只能一个线程进入。
如果你的场景是读多写少,排他锁就太浪费了。
privatefinalReadWriteLockrwLock=newReentrantReadWriteLock();privatefinalLockreadLock=rwLock.readLock();privatefinalLockwriteLock=rwLock.writeLock();// 读操作(多个线程可以同时读)publicintget(){readLock.lock();try{returncount;}finally{readLock.unlock();}}// 写操作(独占)publicvoidset(intvalue){writeLock.lock();try{count=value;}finally{writeLock.unlock();}}原理:读锁是共享锁,写锁是排他锁。读的时候其他线程也能读,写的时候其他线程都不能操作。
踩坑三:ReadWriteLock 的读锁不是绝对并发的
读锁虽然是共享的,但如果有一个线程持有读锁,此时有写锁请求进来,后续的读请求会阻塞——这叫写饥饿。
解决方案:用 StampedLock 的乐观读模式。
六、高频面试 Q&A
Q1:synchronized 和 ReentrantLock 的区别?
甩结论 + 逐条解释:
- synchronized 是 JVM 内置锁,自动释放
- ReentrantLock 是 JUC 锁,手动释放,需要 finally
- synchronized 不支持公平锁、公平锁、可中断
- ReentrantLock 支持 tryLock、公平锁、lockInterruptibly
- ReentrantLock 有多 Condition,synchronized 只有单个 wait 队列
Q2:synchronized 可以完全替代 ReentrantLock 吗?
不能。以下场景必须用 ReentrantLock:
- 需要公平锁(synchronized 不支持)
- 需要可中断等待(lockInterruptibly)
- 需要 tryLock 尝试获取(不阻塞)
- 需要多个等待队列(多 Condition)
- 需要 ReadWriteLock/StampedLock 优化读多写少场景
Q3:synchronized 锁升级过程?
无锁 → 偏向锁(单线程)→ 轻量级锁(多线程自旋)→ 重量级锁(OS Mutex)
关键:只能升级不能降级。
Q4:为什么 JDK 1.6 后 synchronized 性能变好了?
因为加入了偏向锁和轻量级锁优化。大部分场景锁只被一个线程持有,偏向锁零开销;多线程交替执行时用轻量级锁自旋,不需要进入内核态。
七、记忆口诀
synchronized:自动、简单、够用 ReentrantLock:手动、复杂、功能全 AQS:state + CLH队列,JUC的基石 读多写少:ReadWriteLock tryLock+lockInterruptibly+多Condition,选ReentrantLock八、选型总结
| 场景 | 推荐 |
|---|---|
| 简单同步,代码要简洁 | synchronized |
| 需要公平锁 | ReentrantLock |
| 需要可中断等待 | ReentrantLock |
| 需要 tryLock 限时获取 | ReentrantLock |
| 读多写少,高并发读 | ReadWriteLock |
| 需要乐观读 + 写锁 | StampedLock |
| 分布式环境跨 JVM | Redis 分布式锁(都不是这两种) |
最后一句话:synchronized 能用就用,它经过了 JVM 这么多年优化,简单安全。大多数场景不需要换 Lock,除非你有明确的高级特性需求。