1. 为什么我们需要深入理解Java并发锁机制?
在Java开发中,我经常看到很多开发者对synchronized关键字用得得心应手,但当遇到更复杂的并发场景时却束手无策。实际上,Java并发包(java.util.concurrent)中的锁机制提供了更强大、更灵活的控制能力。作为在Java一线开发摸爬滚打多年的老手,我发现真正理解ReentrantLock和AQS的实现原理,能让你在解决复杂并发问题时游刃有余。
记得有一次排查线上死锁问题,正是靠着对AQS内部队列机制的深入理解,才能在短时间内定位到问题根源。这种底层知识在实际工作中往往能起到关键作用。不同于简单的API调用,理解锁的实现原理能让你:
- 更准确地选择合适的锁策略
- 更高效地诊断和解决死锁问题
- 在性能调优时做出更明智的决策
- 必要时能够扩展或自定义锁机制
2. ReentrantLock的核心特性解析
2.1 基础使用与注意事项
让我们从一个典型的使用场景开始:
java复制public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void safeIncrement() {
lock.lock(); // 获取锁
try {
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
} finally {
lock.unlock(); // 释放锁
}
}
}
关键提示:必须在finally块中释放锁,否则一旦发生异常,锁将无法释放,导致死锁。这是新手常犯的错误。
2.2 可重入性详解
可重入性是ReentrantLock的核心特性之一。它允许同一个线程多次获取同一把锁:
java复制public void recursiveMethod(int n) {
lock.lock();
try {
if (n <= 0) return;
System.out.println("Enter level " + n);
recursiveMethod(n - 1); // 递归调用,会再次获取同一把锁
} finally {
lock.unlock();
}
}
这种特性避免了线程自己阻塞自己的情况,特别适合递归调用或需要嵌套加锁的场景。
2.3 公平锁与非公平锁的选择
ReentrantLock提供了两种模式:
- 公平锁:严格按照请求顺序分配锁
- 非公平锁:允许"插队",可能提高吞吐量
java复制// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);
// 创建非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock();
在实际项目中,我发现:
- 高竞争环境下,公平锁能避免线程饥饿,但性能较差
- 低竞争环境下,非公平锁能减少线程切换,提高吞吐量
- 默认使用非公平锁,除非有明确的顺序性需求
3. AQS深度解析:Java并发的心脏
3.1 AQS的核心设计
AQS(AbstractQueuedSynchronizer)是Java并发包的基石。它的核心思想是:
- 用一个volatile int state表示同步状态
- 通过FIFO队列管理等待线程
- 使用模板方法模式,让子类实现关键操作
java复制// AQS简化结构
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态
private transient volatile Node head; // 队列头
private transient volatile Node tail; // 队列尾
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
}
3.2 状态(state)的灵活运用
AQS的state字段是其最精妙的设计之一。不同的同步器以不同方式解释这个状态:
| 同步器 | state含义 | 说明 |
|---|---|---|
| ReentrantLock | 重入次数 | 0表示未锁定,>0表示锁定次数 |
| Semaphore | 可用许可数 | 初始值为最大许可数 |
| CountDownLatch | 剩余计数 | 计数到0时释放所有等待线程 |
| ReadWriteLock | 读写状态 | 高16位读锁数,低16位写锁数 |
3.3 队列管理机制
AQS使用CLH队列的变体管理等待线程。这个队列有几个关键特点:
- 双向链表结构,便于节点移除
- 前驱节点的状态决定后续节点的行为
- 通过自旋和park/unpark实现高效的线程阻塞与唤醒
java复制// 典型的锁获取流程
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加入队列并等待
selfInterrupt(); // 恢复中断状态
}
4. ReentrantLock与AQS的完美配合
4.1 公平锁与非公平锁的实现差异
非公平锁的实现更"激进":
java复制final void lock() {
if (compareAndSetState(0, 1)) // 直接尝试获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
而公平锁会先检查队列:
java复制protected final boolean tryAcquire(int acquires) {
if (!hasQueuedPredecessors() && // 检查是否有等待线程
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
// 重入逻辑...
}
4.2 完整的锁获取流程
- 尝试直接获取锁(tryAcquire)
- 失败后创建节点并入队(addWaiter)
- 在队列中等待(acquireQueued)
- 被唤醒后再次尝试获取锁
java复制final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 只有前驱是头节点才尝试
setHead(node);
p.next = null; // 帮助GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
5. 条件变量(Condition)的实现原理
5.1 条件队列与同步队列
AQS维护两种队列:
- 同步队列:所有等待获取锁的线程
- 条件队列:等待特定条件的线程
java复制public class ConditionObject implements Condition {
private transient Node firstWaiter; // 条件队列头
private transient Node lastWaiter; // 条件队列尾
public final void await() throws InterruptedException {
Node node = addConditionWaiter(); // 加入条件队列
int savedState = fullyRelease(node); // 完全释放锁
// 等待被signal唤醒...
}
}
5.2 signal/signalAll的工作机制
当调用signal()时:
- 将节点从条件队列转移到同步队列
- 唤醒该节点对应的线程
- 线程将在同步队列中重新竞争锁
java复制public final void signal() {
Node first = firstWaiter;
if (first != null)
doSignal(first); // 转移第一个等待节点
}
6. 实战经验与性能优化
6.1 锁优化策略
在实际项目中,我总结了以下优化经验:
- 减少锁粒度:将大锁拆分为多个小锁
- 锁分离:读写锁分离(ReadWriteLock)
- 减少锁持有时间:只锁必要的代码段
- 无锁编程:考虑使用Atomic类或ConcurrentHashMap
6.2 死锁诊断技巧
使用ThreadMXBean可以检测死锁:
java复制ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] threadIds = bean.findDeadlockedThreads();
if (threadIds != null) {
ThreadInfo[] infos = bean.getThreadInfo(threadIds);
for (ThreadInfo info : infos) {
System.out.println("Deadlocked thread: " + info.getThreadName());
System.out.println("Waiting for: " + info.getLockName());
}
}
6.3 选择合适的锁策略
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 高竞争写操作 | ReentrantLock(非公平) | 吞吐量高 |
| 读多写少 | ReadWriteLock | 读操作可并发 |
| 顺序性要求高 | ReentrantLock(公平) | 保证公平性 |
| 简单同步 | synchronized | 编码简单 |
7. AQS在Java并发框架中的应用
7.1 同步器家族
AQS是许多并发工具的基础:
| 工具类 | 实现特点 |
|---|---|
| Semaphore | 共享模式,state表示许可数 |
| CountDownLatch | 一次性屏障,state表示计数 |
| CyclicBarrier | 可重复使用的屏障 |
| ReentrantReadWriteLock | 读写锁分离实现 |
7.2 自定义同步器示例
基于AQS实现一个简单的二元闭锁:
java复制public class BinaryLatch {
private static class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int acquires) {
return getState() == 1 ? 1 : -1; // 闭锁打开时返回1
}
protected boolean tryReleaseShared(int releases) {
setState(1); // 打开闭锁
return true;
}
}
private final Sync sync = new Sync();
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void open() {
sync.releaseShared(1);
}
}
8. 深入理解AQS的设计哲学
通过分析ReentrantLock和AQS,我们可以领悟到以下设计精髓:
- 模板方法模式:固定算法骨架,允许子类扩展关键步骤
- 队列管理:CLH变体队列平衡了公平性和性能
- 状态抽象:一个int字段通过不同解释支持多种同步器
- 性能与公平的权衡:非公平锁提高吞吐,公平锁保证顺序
在实际开发中,我发现理解这些设计哲学比记住API更有价值。它们能帮助你在面对新的并发问题时,快速找到解决方案的思路。比如,当需要实现一个自定义的同步原语时,继承AQS往往是比从头开始更可靠的选择。