引言
锁是 Java 并发编程的基础设施。从最古老的 `synchronized` 到 JDK 5 引入的 `ReentrantLock`,再到 JDK 8 的 `StampedLock`,Java 的锁机制经历了从 JVM 内置到 API 化再到乐观读的演进。理解每种锁的原理和适用场景,是写出高效并发代码的前提。
一、synchronized
1.1 基本用法
```java
// 1. 修饰实例方法:锁当前实例
public synchronized void method() {
// 同步代码
}
// 2. 修饰静态方法:锁当前类的 Class 对象
public static synchronized void staticMethod() {
// 同步代码
}
// 3. 修饰代码块:锁指定对象
public void method() {
synchronized (lock) {
// 同步代码
}
}
```
1.2 锁的原理:Monitor
```
每个 Java 对象关联一个 Monitor(监视器)
synchronized 进入:
monitorenter → 获取 Monitor 锁
synchronized 退出:
monitorexit → 释放 Monitor 锁
Monitor 结构:
┌────────────────────────────┐
│ _owner → 持有锁的线程 │
│ _entry_set → 等待获取锁 │
│ _wait_set → 调用 wait() │
└────────────────────────────┘
```
1.3 锁升级(偏向锁 → 轻量级锁 → 重量级锁)
JDK 6 引入锁升级优化,`synchronized` 不再总是重量级锁:
```
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
│ │ │ │
│ │ │ └─ 竞争激烈,OS 互斥量
│ │ └─ 自旋等待(CAS),适合短时间竞争
│ └─ 同一线程重复获取,无竞争,CAS 都不需要
└─ 对象刚创建,无任何线程访问
升级是单向的(不可降级,G1 下有例外)
```
| 锁状态 | 适用场景 | 获取方式 | 性能 |
|--------|----------|----------|------|
| 偏向锁 | 同一线程反复获取 | 检查偏向线程 ID | 最优 |
| 轻量级锁 | 交替执行,低竞争 | CAS 自旋 | 优 |
| 重量级锁 | 高竞争 | OS 互斥量 | 差 |
1.4 优缺点
| 优点 | 缺点 |
|------|------|
| JVM 内置,使用简单 | 无法中断等待锁的线程 |
| 锁升级自动优化 | 无法设置超时 |
| 异常自动释放锁 | 不支持公平锁 |
| 可重入 | 不支持条件变量(Condition) |
| | 在虚拟线程中会导致固定(Pinning) |
二、ReentrantLock
2.1 基本用法
```java
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 同步代码
} finally {
lock.unlock(); // 必须在 finally 中释放
}
}
```
2.2 公平锁 vs 非公平锁
```java
// 非公平锁(默认):新来的线程可能插队
ReentrantLock unfairLock = new ReentrantLock();
// 公平锁:按等待顺序获取锁
ReentrantLock fairLock = new ReentrantLock(true);
```
| 对比 | 非公平锁 | 公平锁 |
|------|----------|--------|
| 获取顺序 | 可能插队 | FIFO 排队 |
| 吞吐量 | 高 | 低 |
| 饥饿风险 | 有(线程可能永远等不到) | 无 |
| 实现原理 | CAS 直接尝试 | 先入队列排队 |
2.3 可中断锁
```java
ReentrantLock lock = new ReentrantLock();
Thread t = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可被中断的锁获取
try {
doWork();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("被中断,放弃获取锁");
}
});
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断等待锁的线程
```
2.4 超时获取锁
```java
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) {
try {
doWork();
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败,执行降级逻辑");
}
// 带超时
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
doWork();
} finally {
lock.unlock();
}
} else {
System.out.println("5秒内未获取锁");
}
```
2.5 Condition 条件变量
```java
// synchronized 只有一个 wait set
// ReentrantLock 支持多个 Condition,实现精准唤醒
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object\[\] items = new Object10;
private int count, putIdx, takeIdx;
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 队列满,等待 notFull 条件
}
itemsputIdx = item;
if (++putIdx == items.length) putIdx = 0;
count++;
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 队列空,等待 notEmpty 条件
}
Object item = itemstakeIdx;
itemstakeIdx = null;
if (++takeIdx == items.length) takeIdx = 0;
count--;
notFull.signal(); // 通知生产者
return item;
} finally {
lock.unlock();
}
}
```
2.6 synchronized vs ReentrantLock
| 对比 | synchronized | ReentrantLock |
|------|-------------|---------------|
| 实现 | JVM 内置 | JDK API(AQS) |
| 锁获取 | 自动 | 手动 lock/unlock |
| 可中断 | ❌ | ✅ lockInterruptibly |
| 超时 | ❌ | ✅ tryLock(timeout) |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件变量 | wait/notify(一个) | Condition(多个) |
| 锁升级 | 偏向→轻量→重量 | 无 |
| 虚拟线程 | 固定(Pinning) | 正常卸载 |
| 异常安全 | 自动释放 | 需 finally 手动释放 |
三、ReadWriteLock
3.1 读写分离
```java
// 读读不互斥,读写互斥,写写互斥
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private Map<String, Data> cache = new HashMap<>();
public Data get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(String key, Data value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
```
3.2 ReentrantReadWriteLock 特性
| 特性 | 说明 |
|------|------|
| 公平性 | 支持公平/非公平模式 |
| 重入 | 读锁和写锁都支持重入 |
| 锁降级 | 写锁可降级为读锁(获取写锁→获取读锁→释放写锁) |
| 锁升级 | 读锁不能升级为写锁(会死锁) |
```java
// 锁降级:写锁 → 读锁
writeLock.lock();
try {
updateData();
readLock.lock(); // 获取读锁
} finally {
writeLock.unlock(); // 释放写锁,此时持有读锁
}
try {
readData(); // 在读锁保护下读取
} finally {
readLock.unlock();
}
```
3.3 读写锁的问题
-
**写饥饿**:读操作频繁时,写线程可能长时间获取不到写锁
-
**不支持乐观读**:即使只是读取,也需要获取读锁
四、StampedLock
4.1 核心思想
StampedLock 在 ReadWriteLock 基础上引入了**乐观读(Optimistic Read)**:
```
ReadWriteLock:
读锁 → 获取读锁 → 读取 → 释放读锁(每次都要 CAS)
StampedLock:
乐观读 → 获取戳 → 读取 → 验证戳 → 成功则无需 CAS
→ 失败则升级为读锁重试
```
4.2 三种模式
| 模式 | 说明 | 类比 |
|------|------|------|
| 写锁(Writing) | 独占锁,与读锁互斥 | ReadWriteLock 的写锁 |
| 读锁(Reading) | 共享锁,与写锁互斥 | ReadWriteLock 的读锁 |
| 乐观读(Optimistic Reading) | 无锁读取,验证后使用 | 数据库的乐观锁 |
4.3 基本用法
```java
private final StampedLock sl = new StampedLock();
private double x, y;
// 写操作
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 乐观读
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 1. 获取乐观读戳
double currentX = x, currentY = y; // 2. 读取数据(无锁)
if (!sl.validate(stamp)) { // 3. 验证戳(期间是否有写操作?)
stamp = sl.readLock(); // 4. 验证失败,升级为读锁
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 悲观读
double distanceFromOriginPessimistic() {
long stamp = sl.readLock();
try {
return Math.sqrt(x * x + y * y);
} finally {
sl.unlockRead(stamp);
}
}
```
4.4 锁转换
```java
// 读锁 → 写锁
long stamp = sl.readLock();
try {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
// 持有写锁
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
} finally {
sl.unlock(stamp);
}
// 乐观读 → 读锁
long stamp = sl.tryOptimisticRead();
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
// 重新读取
} finally {
sl.unlockRead(stamp);
}
}
```
4.5 StampedLock 注意事项
| 注意 | 说明 |
|------|------|
| 不可重入 | 同一线程不能重复获取同一锁 |
| 不支持 Condition | 没有 newCondition() 方法 |
| 不要用 interrupt | 不要在 readLock/writeLock 中调用 interrupt |
| unlock 必须传 stamp | 传入错误的 stamp 会抛 IllegalMonitorStateException |
| 乐观读适合读多写少 | 写频繁时乐观读经常失败,反而更慢 |
五、锁选型指南
5.1 决策树
```
需要锁吗?
├─ 能否用无锁方案?(Atomic* / Concurrent集合)
│ ├─ 能 → 优先无锁
│ └─ 不能 ↓
├─ 读多写少?
│ ├─ 是 → 读远多于写?
│ │ ├─ 是 → StampedLock(乐观读)
│ │ └─ 否 → ReentrantReadWriteLock
│ └─ 否 ↓
├─ 需要高级功能?(中断/超时/多条件/公平)
│ ├─ 是 → ReentrantLock
│ └─ 否 → synchronized(简单场景优先)
├─ 在虚拟线程中使用?
│ ├─ 是 → ReentrantLock(避免 synchronized 固定)
│ └─ 否 → synchronized
```
5.2 性能对比(大致参考)
```
低竞争:synchronized ≈ ReentrantLock > StampedLock
中竞争:StampedLock > ReentrantLock > synchronized
高竞争:StampedLock(乐观读) >> ReentrantReadWriteLock > ReentrantLock > synchronized
读多写少:StampedLock(乐观读) >> ReentrantReadWriteLock >> synchronized
```
5.3 速查表
| 场景 | 推荐锁 | 原因 |
|------|--------|------|
| 简单互斥 | synchronized | 简单,JVM 优化好 |
| 需要中断/超时 | ReentrantLock | lockInterruptibly / tryLock |
| 需要公平锁 | ReentrantLock(true) | synchronized 不支持 |
| 需要多条件唤醒 | ReentrantLock + Condition | synchronized 只有一个 wait set |
| 读多写少 | StampedLock | 乐观读无锁,性能最优 |
| 虚拟线程中 | ReentrantLock | synchronized 导致固定 |
| 缓存场景 | ReentrantReadWriteLock | 读写分离 |
| 高并发点数据 | StampedLock | 乐观读 + 锁转换 |
六、锁的性能优化技巧
6.1 减小锁粒度
```java
// ❌ 锁范围过大
synchronized (this) {
validate(data); // 无需锁
process(data); // 需要锁
log(data); // 无需锁
}
// ✅ 只锁必要部分
validate(data);
synchronized (this) {
process(data);
}
log(data);
```
6.2 减小锁粒度(分段锁)
```java
// ❌ 一把大锁
synchronized (allAccounts) {
transfer(from, to, amount);
}
// ✅ 分段锁(类似 ConcurrentHashMap)
private final Object\[\] locks = new Object16;
{
for (int i = 0; i < locks.length; i++) locksi = new Object();
}
void transfer(Account from, Account to, int amount) {
Object lock1 = locksMath.abs(from.hashCode() % 16);
Object lock2 = locksMath.abs(to.hashCode() % 16);
// 按固定顺序加锁,避免死锁
Object first = System.identityHashCode(lock1) < System.identityHashCode(lock2) ? lock1 : lock2;
Object second = first == lock1 ? lock2 : lock1;
synchronized (first) {
synchronized (second) {
from.debit(amount);
to.credit(amount);
}
}
}
```
6.3 避免锁嵌套
```java
// ❌ 锁嵌套,容易死锁
synchronized (lockA) {
synchronized (lockB) {
doWork();
}
}
// ✅ 拆分:先获取所有锁,再操作
// 或使用 tryLock 避免无限等待
if (lockA.tryLock()) {
try {
if (lockB.tryLock()) {
try {
doWork();
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
```
总结
| 锁 | 一句话 | 适用场景 |
|----|--------|----------|
| synchronized | 简单可靠,JVM 优化 | 简单互斥,低竞争 |
| ReentrantLock | 功能丰富,API 灵活 | 需要中断/超时/公平/多条件 |
| ReentrantReadWriteLock | 读写分离 | 读多写少,但写不极少 |
| StampedLock | 乐观读,极致性能 | 读远多于写,追求极致吞吐 |