从synchronized到ReentrantLock,我终于理清了Java锁的那些事
从synchronized到ReentrantLock,我终于理清了Java锁的那些事
Java 并发编程里,锁是最常用的同步机制。但 synchronized 和 ReentrantLock 有什么区别?什么时候该用哪个?锁升级是怎么回事?偏向锁为什么被废弃?
这些问题在面试里经常被问到,但在实际项目中踩过坑才真正理解。
synchronized 的演进
很多人以为 synchronized 就是最老的那套重量级锁实现,性能很差。这是个误解。
JVM 对 synchronized 做了大量优化,包括偏向锁、轻量级锁、适应性自旋等。
锁的四种状态
synchronized 有四种锁状态,级别从低到高:
- 无锁:没有线程访问共享资源
- 偏向锁:只有一个线程反复进入同步块
- 轻量级锁:多个线程交替进入同步块,没有并发争抢
- 重量级锁:多个线程并发争抢共享资源
锁只能升级,不能降级(除非偏向锁被撤销后可以重偏向)。
偏向锁的原理
当一个线程第一次进入同步块时,在对象头的 Mark Word 里记录这个线程的 ID。之后这个线程再进入同步块,只需要比较 Mark Word 里的线程 ID,不需要加锁和释放锁。
public class偏向锁演示 {
private final Object lock = new Object();
public void method() {
synchronized (lock) {
// 第一次进入:记录线程ID到Mark Word
// 之后进入:比较Mark Word中的线程ID
}
}
}
偏向锁的优点是:如果同步块一直被同一个线程访问,这个线程几乎零成本。
偏向锁的问题
偏向锁在多线程并发时会有问题:如果一个线程获得偏向锁后,另一个线程尝试进入同步块,需要撤销偏向锁。这个过程会 Stop The World(STW),在某些场景下反而更慢。
所以 JDK 15 废弃了偏向锁,JDK 18 直接移除了。
轻量级锁
当有多个线程竞争时,JVM 会尝试用 CAS(Compare And Swap)把锁升级为轻量级锁。
线程在进入同步块前,在自己的栈帧里创建一个锁记录(Lock Record),然后用 CAS 把对象头的 Mark Word 指向这个锁记录:
// 轻量级锁加锁过程(伪代码)
public void lock() {
// 1. 在当前线程栈帧创建锁记录
LockRecord lockRecord = new LockRecord();
// 2. 用CAS尝试把对象头的Mark Word更新为指向锁记录的指针
if (casUpdateMarkWord(lockRecord)) {
// 成功:当前线程获得了轻量级锁
} else {
// 失败:可能有其他线程在竞争,升级为重量级锁
inflateToHeavyweightLock();
}
}
轻量级锁适用于“线程交替执行同步块”的场景,没有实际的操作系统 mutex 参与。
重量级锁
当轻量级锁 CAS 失败,或者有线程在持有轻量级锁时阻塞,锁会膨胀为重量级锁。
重量级锁依赖操作系统的 Mutex 实现,需要用户态到内核态的切换,开销较大。
// synchronized 默认的锁升级过程
public class锁升级演示 {
private final Object lock = new Object();
public void method() {
synchronized (lock) {
// 第一次:无锁 → 偏向锁(记录线程ID)
// 第二次进入:偏向锁(比较线程ID)
// 有其他线程竞争:偏向锁 → 轻量级锁(CAS)
// CAS失败或 contention:轻量级锁 → 重量级锁
}
}
}
ReentrantLock vs synchronized
基本用法对比
// synchronized 用法
public class SynchronizedDemo {
private final Object lock = new Object();
public void method() {
synchronized (lock) {
// 临界区
}
}
}
// ReentrantLock 用法
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须手动释放
}
}
}
关键区别
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁释放 | 自动释放 | 必须手动 unlock() |
| 尝试获取 | 阻塞 | tryLock() 可非阻塞尝试 |
| 超时等待 | 不支持 | tryLock(timeout) 支持 |
| 公平锁 | 不支持 | 可配置公平/非公平 |
| 可中断 | 不支持 | lockInterruptibly() 支持 |
| 条件变量 | 无 | newCondition() 支持多个 |
tryLock 非阻塞获取
ReentrantLock 支持非阻塞获取锁:
public class TryLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
// 尝试获取锁,立即返回
if (lock.tryLock()) {
try {
// 获取成功
} finally {
lock.unlock();
}
} else {
// 获取失败,做降级处理
fallback();
}
}
}
tryLock 带超时
获取锁时设置超时,避免无限等待:
public class TryLockTimeoutDemo {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
try {
// 等待3秒,还拿不到就放弃
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 临界区
} finally {
lock.unlock();
}
} else {
// 超时后的降级处理
handleLockTimeout();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
handleInterrupt();
}
}
}
synchronized 无法实现超时,线程只能阻塞等锁。
可中断锁
lockInterruptibly() 允许在等待过程中响应中断:
public class InterruptibleLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
// 临界区
} finally {
lock.unlock();
}
}
}
如果线程被中断,lockInterruptibly() 会抛出 InterruptedException,而不是继续等待。
公平锁
ReentrantLock 可以创建公平锁,按等待顺序分配锁:
// 公平锁:按等待顺序获取锁
private final ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁(默认):允许插队,可能饥饿
private final ReentrantLock unfairLock = new ReentrantLock(false);
公平锁的开销比非公平锁大,因为需要维护一个等待队列。一般情况下用非公平锁即可。
多个条件变量
synchronized 只有一个条件队列(wait/notify),ReentrantLock 可以创建多个条件变量:
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列不满
private final Condition notEmpty = lock.newCondition(); // 队列不空
private final Object[] items = new Object[100];
private int count = 0;
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 队列满,等待
}
items[count++] = item;
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 队列空,等待
}
Object item = items[--count];
notFull.signal(); // 唤醒生产者
return item;
} finally {
lock.unlock();
}
}
}
这实现了一个有界阻塞队列,生产者和消费者用不同的条件变量等待。
读写锁优化并发读
如果业务场景是读多写少,用 ReentrantReadWriteLock 可以进一步提升性能:
public class CacheDemo {
private final Map<String, String> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作:共享锁,多线程可以并发读
public String get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
// 写操作:独占锁
public void put(String key, String value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
读写锁的规则:
- 读锁共享:多个线程可以同时持有读锁
- 写锁独占:持有写锁时,其他线程不能持有任何锁
- 读写互斥:读锁和写锁不能同时持有
什么时候用什么锁
用 synchronized 的场景
- 简单同步需求:只需要互斥,不需要超时、中断等高级特性
- 代码块很小:synchronized 会自动优化,短临界区影响小
- 不需要公平锁:synchronized 不支持公平策略
// synchronized 适用场景
public class SimpleCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
用 ReentrantLock 的场景
- 需要超时或尝试获取:用 tryLock()
- 需要可中断等待:用 lockInterruptibly()
- 需要公平锁:用 new ReentrantLock(true)
- 需要多个条件变量:用 newCondition()
- 读多写少:用 ReentrantReadWriteLock
// ReentrantLock 适用场景
public class InventoryService {
private final ReentrantLock lock = new ReentrantLock();
private final Map<Long, Integer> inventory = new ConcurrentHashMap<>();
public boolean deduct(Long productId, int quantity) {
lock.lock();
try {
int stock = inventory.getOrDefault(productId, 0);
if (stock < quantity) {
return false;
}
inventory.put(productId, stock - quantity);
return true;
} finally {
lock.unlock();
}
}
}
性能对比
在低并发场景下,synchronized 和 ReentrantLock 性能差别不大。
在高并发场景:
- 无竞争:synchronized(偏向锁)性能最好
- 轻度竞争:轻量级锁 CAS 性能好
- 重度竞争:两者都需要 OS 介入,性能相近
JDK 16+ 对 synchronized 做了大量优化(锁膨胀优化、自适应偏向),在大多数场景下性能已经不输 ReentrantLock。
总结
Java 锁的核心知识点:
- synchronized 有锁升级:偏向锁 → 轻量级锁 → 重量级锁(偏向锁已被废弃)
- ReentrantLock 更灵活:支持超时、尝试获取、可中断、公平锁
- 读写锁适合读多写少:ReentrantReadWriteLock
- 选择依据:简单互斥用 synchronized,需要高级特性用 ReentrantLock
实际项目中,大多数 synchronized 的场景已经足够用。但如果需要更精细的锁控制,ReentrantLock 和 ReentrantReadWriteLock 是更好的选择。
