Java 锁机制:synchronized、ReentrantLock 与 AQS 全解析
一、锁的核心概念与分类
1. 为什么需要锁?
- 竞态条件:多线程并发访问共享资源导致数据不一致
- 内存可见性:保证线程修改对其他线程立即可见
2. 锁的分类维度
分类标准 | 锁类型 | 代表实现 |
---|---|---|
乐观/悲观 | 乐观锁、悲观锁 | CAS vs synchronized |
公平性 | 公平锁、非公平锁 | ReentrantLock(true/false) |
可重入性 | 可重入锁、不可重入锁 | synchronized |
阻塞/非阻塞 | 阻塞锁、自旋锁 | AQS vs CAS |
二、synchronized 深度解析
1. 使用方式
java
// 实例方法锁
public synchronized void method() {}
// 静态方法锁
public static synchronized void staticMethod() {}
// 代码块锁
synchronized(lockObject) {
// 临界区
}
2. 底层实现原理
-
Monitor 机制:每个对象关联一个Monitor
-
对象头Mark Word结构:
锁状态 存储内容 无锁 对象hashCode 偏向锁 线程ID + Epoch 轻量级锁 指向栈中锁记录的指针 重量级锁 指向Monitor的指针
-
3. 锁升级过程
- 偏向锁 (单线程访问)
- 通过CAS设置线程ID
- 无需同步开销
- 轻量级锁 (多线程交替访问)
- 通过CAS竞争锁记录指针
- 失败后自旋尝试
- 重量级锁 (激烈竞争)
- 线程阻塞,进入等待队列
- 依赖操作系统mutex
三、ReentrantLock 机制剖析
1. 核心特性对比
特性 | synchronized | ReentrantLock |
---|---|---|
实现机制 | JVM内置 | JDK实现(AQS) |
锁获取方式 | 自动获取释放 | 必须手动lock/unlock |
可中断 | 不支持 | lockInterruptibly()支持 |
公平锁 | 非公平 | 可配置公平/非公平 |
条件队列 | 单条件 | 多Condition |
2. 源码解析(非公平锁实现)
java
final void lock() {
if (compareAndSetState(0, 1)) // 快速尝试获取
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 进入AQS队列
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 重入计数
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
四、AQS(AbstractQueuedSynchronizer)原理
1. AQS核心结构
java
// 关键字段
private volatile int state; // 同步状态
private transient volatile Node head; // CLH队列头
private transient volatile Node tail; // CLH队列尾
// Node节点结构
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter; // 条件队列专用
}
2. 工作流程(以ReentrantLock为例)
- 获取锁 :
- 尝试CAS修改state
- 失败后加入CLH队列
- 自旋检查前驱节点状态
- 释放锁 :
- 修改state并唤醒后继节点
- 取消中断标记
3. 关键方法模板
java
// 需要子类实现
protected boolean tryAcquire(int arg) { ... }
// AQS提供的模板方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
五、锁的性能优化策略
1. 减少锁竞争的方法
策略 | 实现方式 | 适用场景 |
---|---|---|
锁细化 | 缩小同步代码块范围 | 大对象的部分字段保护 |
锁分离 | 读写锁分离(ReadWriteLock) | 读多写少场景 |
无锁编程 | 使用Atomic类 | 简单状态变更 |
ThreadLocal | 线程私有变量 | 避免共享资源 |
2. 锁性能对比测试数据
锁类型 | 吞吐量(ops/ms) | 延迟(ns) |
---|---|---|
无锁 | 1589 | 62 |
偏向锁 | 1342 | 74 |
轻量级锁 | 987 | 101 |
synchronized | 563 | 177 |
ReentrantLock | 621 | 160 |
六、锁的常见问题与解决方案
💬 Q1:synchronized和ReentrantLock如何选择?
✅ 答案:
- 优先使用
synchronized
:简单场景、JVM自动优化 - 选择
ReentrantLock
:需要高级功能(可中断、公平锁、条件队列)
💬 Q2:什么是锁膨胀?如何避免?
✅ 答案:
- 现象:从偏向锁→轻量级锁→重量级锁的升级过程
- 避免方法 :
- 减少同步代码块执行时间
- 降低锁竞争强度(如减小并发线程数)
💬 Q3:AQS为什么用CLH队列?
✅ 答案:
- 前驱节点感知:通过前驱节点的状态减少自旋消耗
- 无锁设计:队列操作基于CAS,减少同步开销
- 公平性保证:严格FIFO顺序
最佳实践建议:
- 使用
jstack
分析锁竞争情况 - 避免在锁内调用外部方法(防止死锁)
- 高并发场景优先考虑无锁方案(如
ConcurrentHashMap
)