
Java并发编程:Lock体系 系统性知识总结
一、Lock体系整体概述
1.1 产生背景
synchronized是Java内置的锁机制,但存在以下局限性:
- 不可中断:获取锁的线程会一直阻塞,无法响应中断
- 非公平:默认非公平,无法实现公平锁
- 单一条件:只能关联一个条件变量
- 无法尝试获取锁:不能设置超时时间,获取失败会无限阻塞
1.2 Lock接口定义
java.util.concurrent.locks.Lock是所有锁的顶级接口,定义了锁的基本操作:
public interface Lock {
void lock(); // 阻塞获取锁
void lockInterruptibly() throws InterruptedException; // 可中断获取锁
boolean tryLock(); // 非阻塞尝试获取锁,立即返回
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时尝试获取锁
void unlock(); // 释放锁
Condition newCondition(); // 创建条件变量
}
1.3 Lock体系核心类图
Lock
├─ ReentrantLock (可重入锁)
├─ ReadWriteLock (读写锁接口)
│ └─ ReentrantReadWriteLock (可重入读写锁)
└─ StampedLock (JDK8新增,乐观读写锁)
二、ReentrantLock详解
2.1 基本特性
- 可重入性:同一个线程可以多次获取同一把锁,内部维护了一个计数器
- 支持公平/非公平模式:构造方法可指定,默认非公平
- 可中断:支持
lockInterruptibly()方法 - 超时获取:支持
tryLock(long, TimeUnit)方法 - 多条件变量:支持创建多个
Condition对象
2.2 实现原理:基于AQS
ReentrantLock内部通过AbstractQueuedSynchronizer(AQS)实现,AQS是Java并发包的基础框架:
- 同步状态:
private volatile int state,0表示未锁定,>0表示已锁定 - CLH队列:双向链表结构,用于存放等待锁的线程
- 独占模式:ReentrantLock使用AQS的独占模式
2.3 公平锁 vs 非公平锁
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 严格按照线程等待顺序 | 先尝试直接获取锁,失败再排队 |
| 性能 | 较低(上下文切换多) | 较高(减少上下文切换) |
| 饥饿问题 | 无 | 可能出现(线程一直抢不到锁) |
| 实现方式 | new ReentrantLock(true) |
new ReentrantLock(false)(默认) |
非公平锁获取流程:
- 尝试CAS将state从0改为1,成功则直接获取锁
- 失败则调用
acquire(1)进入AQS队列 - 队列中的线程按顺序获取锁
公平锁获取流程:
- 直接调用
acquire(1) - 先检查队列是否有等待线程,有则排队
- 没有等待线程才尝试CAS获取锁
2.4 可重入性实现
当线程再次获取锁时:
- 检查当前线程是否是持有锁的线程
- 如果是,将state加1
- 释放锁时,state减1,直到state为0才真正释放锁
2.5 Condition条件变量
- 替代了Object的wait()/notify()/notifyAll()方法
- 一个Lock可以创建多个Condition,实现更精细的线程通信
- 常用方法:
await()、signal()、signalAll()
使用示例:
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 队列满,等待
}
queue.add(item);
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
// 消费者
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空,等待
}
Object item = queue.remove();
notFull.signal(); // 唤醒生产者
return item;
} finally {
lock.unlock();
}
三、ReentrantReadWriteLock详解
3.1 设计思想
- 读写分离:读操作可以并发执行,写操作必须独占执行
- 适用于读多写少的场景,能显著提高并发性能
- 包含两个锁:读锁(共享锁)和写锁(独占锁)
3.2 基本特性
- 可重入性:读锁和写锁都支持可重入
- 锁降级:写锁可以降级为读锁,但读锁不能升级为写锁
- 公平/非公平模式:构造方法可指定,默认非公平
- 写锁排他:写锁与任何锁都互斥(读锁、写锁)
- 读锁共享:多个线程可以同时持有读锁
3.3 实现原理
ReentrantReadWriteLock内部也基于AQS实现,将32位的state变量拆分为两部分:
- 高16位:表示读锁的持有次数(共享锁计数)
- 低16位:表示写锁的持有次数(独占锁计数)
state = 0x00000000
高16位:读锁计数 低16位:写锁计数
3.4 锁降级机制
定义:持有写锁的线程,可以先获取读锁,再释放写锁,从而将写锁降级为读锁。
目的:保证数据的可见性,避免其他线程在写操作完成后、读操作开始前修改数据。
正确示例:
rwl.writeLock().lock();
try {
// 执行写操作
data = updateData();
// 获取读锁(锁降级)
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // 释放写锁,此时持有读锁
}
try {
// 执行读操作
processData(data);
} finally {
rwl.readLock().unlock(); // 释放读锁
}
注意:不支持锁升级(持有读锁时获取写锁会导致死锁)。
3.5 使用场景与注意事项
- 适用场景:读多写少的缓存系统、配置管理、数据查询服务
- 注意事项:
- 读锁持有期间不能获取写锁(会导致死锁)
- 写锁持有期间可以获取读锁(锁降级)
- 读锁和写锁都必须在finally块中释放
- 写线程过多时可能导致读线程饥饿(可使用公平模式缓解)
四、Lock vs synchronized 全面对比
4.1 核心特性对比
| 特性 | synchronized | Lock |
|---|---|---|
| 实现方式 | JVM内置,字节码层面实现 | JDK层面实现,纯Java代码 |
| 锁释放 | 自动释放(代码块结束或异常) | 手动释放(必须在finally中调用unlock()) |
| 可重入性 | 支持 | 支持(ReentrantLock) |
| 公平性 | 不支持(默认非公平) | 支持(可指定公平/非公平) |
| 可中断性 | 不支持 | 支持(lockInterruptibly()) |
| 超时获取 | 不支持 | 支持(tryLock(long, TimeUnit)) |
| 条件变量 | 只能关联一个(Object的wait/notify) | 支持多个(newCondition()) |
| 锁类型 | 独占锁 | 支持独占锁、共享锁(读写锁) |
| 性能 | 低并发下性能较好 | 高并发下性能更优 |
4.2 实现原理对比
synchronized实现原理:
- 基于对象头的Mark Word和监视器锁(Monitor)
- 锁升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
- 重量级锁依赖操作系统的互斥量(Mutex)实现
Lock实现原理:
- 基于AQS框架和CAS操作
- 内部维护一个CLH队列存放等待线程
- 不依赖操作系统,纯Java实现,避免了用户态与内核态的切换
4.3 使用场景对比
优先使用synchronized的场景:
- 简单的同步场景,代码量少
- 不需要Lock的高级特性(可中断、超时、多条件)
- 低并发环境,synchronized的性能已经足够
- 对代码简洁性要求较高
优先使用Lock的场景:
- 需要公平锁的场景
- 需要可中断获取锁的场景
- 需要超时获取锁的场景
- 需要多个条件变量的场景
- 读多写少的场景(使用ReentrantReadWriteLock)
- 高并发环境,需要更好的性能
五、最佳实践与常见陷阱
5.1 Lock使用最佳实践
必须在finally块中释放锁:防止异常导致锁无法释放
Lock lock = new ReentrantLock(); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }优先使用非公平锁:除非有特殊的公平性要求,否则非公平锁性能更好
合理使用tryLock():避免线程无限阻塞
if (lock.tryLock(1, TimeUnit.SECONDS)) { try { // 业务逻辑 } finally { lock.unlock(); } } else { // 获取锁失败的处理逻辑 }读写锁的正确使用:读操作使用读锁,写操作使用写锁
5.2 常见陷阱
- 忘记释放锁:导致死锁,必须在finally中调用unlock()
- 锁重入次数不匹配:获取多少次锁,就要释放多少次锁
- 在Condition.await()前没有持有锁:会抛出IllegalMonitorStateException
- 读写锁使用错误:读操作使用写锁,导致并发性能下降
- 锁升级:持有读锁时获取写锁,会导致死锁
- 多个锁获取顺序不一致:导致死锁(遵循固定的获取顺序)
六、面试核心考点清单
- ReentrantLock的实现原理:基于AQS,state变量,CLH队列
- 公平锁与非公平锁的区别:获取流程、性能、饥饿问题
- 可重入性的实现:state计数器,线程持有判断
- Condition的作用与实现:替代wait/notify,多条件变量
- ReentrantReadWriteLock的实现原理:state拆分,读写锁互斥规则
- 锁降级机制:定义、目的、正确使用方式
- Lock与synchronized的区别:从实现、特性、性能、使用场景等方面
- AQS的核心思想:同步状态、CLH队列、独占/共享模式
- 死锁的产生条件与避免方法:互斥、持有并等待、不可剥夺、循环等待
- 各种锁的使用场景:根据业务特点选择合适的锁
一、可直接背诵的面试版核心考点(精简版)
1. Lock体系基础
- 产生背景:解决
synchronized的4大缺陷:不可中断、非公平、单一条件、无法超时获取 - Lock接口核心方法:
lock()(阻塞)、lockInterruptibly()(可中断)、tryLock()(非阻塞)、tryLock(long, TimeUnit)(超时)、unlock()(必须finally)、newCondition()(多条件) - 核心实现类:
ReentrantLock(独占可重入)、ReentrantReadWriteLock(读写分离)、StampedLock(JDK8乐观读写)
2. ReentrantLock 必背考点
- 核心特性:可重入、公平/非公平双模式、可中断、超时获取、多条件变量
- 实现原理:基于AQS(抽象队列同步器),
volatile int state记录锁状态,CLH双向队列存放等待线程 - 公平vs非公平:
- 非公平(默认):先CAS抢锁,失败再排队,性能高,可能饥饿
- 公平:直接排队,严格FIFO,性能低,无饥饿
- 可重入实现:同一线程多次获取时
state++,释放时state--,state=0才真正释放 - Condition:替代
wait/notify,一个Lock可创建多个Condition,实现精准唤醒(如生产者消费者的notFull/notEmpty)
3. ReentrantReadWriteLock 必背考点
- 设计思想:读写分离,读共享、写独占,适用于读多写少场景
- state拆分:32位int,高16位=读锁计数,低16位=写锁计数
- 核心规则:
- 写锁与所有锁互斥(读+写)
- 读锁与读锁共享
- 支持锁降级(写→读),不支持锁升级(读→写,会死锁)
- 锁降级目的:保证数据可见性,避免写后读间隙被其他线程修改
4. Lock vs synchronized 核心区别(面试高频)
| 维度 | synchronized | Lock |
|---|---|---|
| 实现层面 | JVM内置,字节码(monitorenter/monitorexit) | JDK层面,纯Java代码(AQS+CAS) |
| 释放方式 | 自动(代码结束/异常) | 手动(必须finally调用unlock()) |
| 公平性 | 仅非公平 | 支持公平/非公平 |
| 可中断性 | 不支持 | 支持(lockInterruptibly()) |
| 超时获取 | 不支持 | 支持(tryLock超时) |
| 条件变量 | 1个(Object) | 多个(Condition) |
| 锁类型 | 仅独占锁 | 独占+共享(读写锁) |
| 性能 | 低并发好,高并发差 | 高并发下显著更优 |
5. 最佳实践与常见陷阱
- 铁律:Lock必须在finally中释放,否则死锁
- 优先非公平锁:除非有严格公平性要求
- tryLock优先:避免无限阻塞
- 读写锁严格区分:读用读锁,写用写锁
- 禁止锁升级:持有读锁时不能获取写锁
- 锁顺序一致:多个锁按固定顺序获取,避免循环等待
二、StampedLock 补充分析(JDK8新增,面试高频对比)
1. 产生背景
解决ReentrantReadWriteLock的写饥饿问题:当读线程非常多时,写线程可能长时间无法获取锁。
2. 核心设计思想
引入乐观读模式:读操作不需要加锁,仅通过一个戳记(stamp)验证数据是否被修改。如果验证通过,直接读取;如果验证失败,再升级为悲观读锁。
3. 三种核心模式
| 模式 | 特性 | 返回值 | 互斥规则 |
|---|---|---|---|
| 写锁(WriteLock) | 独占锁,与所有锁互斥 | 非0戳记 | 与读锁、写锁都互斥 |
| 悲观读锁(ReadLock) | 共享锁,与写锁互斥 | 非0戳记 | 与写锁互斥,与读锁共享 |
| 乐观读(OptimisticRead) | 无锁,仅返回戳记 | 非0戳记 | 不与任何锁互斥 |
4. 乐观读使用示例
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead(); // 获取乐观读戳记
// 读取数据
int data = this.data;
if (!lock.validate(stamp)) {
// 验证戳记是否有效(数据是否被修改)
// 戳记无效,升级为悲观读锁
stamp = lock.readLock();
try {
data = this.data;
} finally {
lock.unlockRead(stamp);
}
}
return data;
5. StampedLock vs ReentrantReadWriteLock 全面对比
| 特性 | ReentrantReadWriteLock | StampedLock |
|---|---|---|
| 可重入性 | 支持 | 不支持 |
| 条件变量 | 支持 | 不支持 |
| 锁降级 | 支持 | 支持(写→悲观读) |
| 锁升级 | 不支持 | 支持(乐观读→悲观读→写) |
| 写饥饿 | 严重(读多写少) | 大幅缓解 |
| 性能 | 读多写少较好 | 读多写少更优(乐观读无锁) |
| 适用场景 | 读多写少,需要重入/条件变量 | 读多写少,追求极致性能 |
6. 使用注意事项
- 乐观读不是锁,必须调用
validate(stamp)验证数据一致性 - 不支持重入,同一线程不能多次获取同一把锁
- 不支持
Condition条件变量 - 写锁和悲观读锁必须使用对应的
unlockWrite(stamp)/unlockRead(stamp)释放 - 戳记无效时必须升级为悲观读锁,不能直接重试乐观读
三、终极面试答题框架(直接套用)
当面试官问"ReentrantLock和synchronized的区别"时,按以下顺序回答:
- 实现层面:JVM内置 vs JDK AQS+CAS
- 释放方式:自动 vs 手动(finally)
- 高级特性:公平性、可中断性、超时获取、多条件变量
- 锁类型:仅独占 vs 独占+共享
- 性能:低并发synchronized好,高并发Lock好
- 适用场景:简单场景用synchronized,需要高级特性用Lock