大家好呀!今天我们要聊的是Java并发编程中一个超级重要的"幕后大佬"——AbstractQueuedSynchronizer(简称AQS)🤵。别看它名字又长又拗口,它可是Java并发包的核心基础,像ReentrantLock、CountDownLatch这些我们常用的工具类,都是基于它实现的哦!✨
一、AQS是什么?🤔
AQS就像是一个万能并发控制器🔧,它提供了一套框架,让开发者可以轻松实现各种同步工具。想象一下,你去银行办业务,AQS就是那个管理排队叫号系统的机器📟,它知道现在谁在办理业务(持有锁),谁在排队(等待队列),以及什么时候该叫下一个号(线程唤醒)。
1.1 为什么需要AQS?
在没有AQS之前,每个同步工具都要自己实现:
- 如何管理线程的排队
- 如何实现线程的阻塞和唤醒
- 如何处理各种边界条件
这就像每家银行都要自己发明一套排队系统,多麻烦啊!😫 AQS的出现,把这些公共逻辑都封装好了,我们只需要告诉它"什么时候能获取锁"、"什么时候要释放锁"就行了。
1.2 AQS的核心思想
AQS的核心可以用三句话概括:
- 状态管理:用一个volatile的int变量表示同步状态🔢
- 排队机制:用CLH队列管理等待线程🚶♂️🚶♀️🚶♂️
- 模板方法:留出关键方法让子类实现✂️
二、AQS的底层原理🔧
2.1 状态变量 - 同步的核心
AQS内部维护了一个重要的变量:
private volatile int state;
这个state
就像银行窗口的"使用状态":
state = 0
:窗口空闲🟢state > 0
:窗口被占用🔴
不同的同步工具对state的解释不同:
- 对于锁:state表示重入次数
- 对于信号量:state表示剩余许可数
- 对于CountDownLatch:state表示还需要countDown的次数
2.2 CLH队列 - 线程排队的地方
当线程获取同步状态失败时,AQS会把线程包装成一个Node节点,加入CLH队列。这个队列有以下几个特点:
- 双向链表:每个节点都知道它的前驱和后继
- 自旋+CAS:通过CAS保证线程安全
- 公平性支持:可以实现公平和非公平两种模式
static final class Node {
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 等待的线程
int waitStatus; // 等待状态
}
2.3 关键方法 - 子类需要实现的
AQS使用了模板方法模式,留了几个关键方法让子类实现:
tryAcquire(int arg)
:尝试获取锁🔒tryRelease(int arg)
:尝试释放锁🔓tryAcquireShared(int arg)
:共享式获取tryReleaseShared(int arg)
:共享式释放
这就像银行给你一个排队系统的框架,你只需要定义:
- 什么条件可以办理业务(tryAcquire)
- 什么条件算业务办完了(tryRelease)
三、AQS的工作原理详解⚙️
3.1 独占模式(如ReentrantLock)
场景:只有一个窗口(资源),多个人要办理业务
获取锁流程🔒:
- 线程调用
acquire(1)
尝试获取锁 - AQS先调用子类的
tryAcquire(1)
方法- 成功:直接返回,线程继续执行
- 失败:进入下一步
- 创建Node节点,加入CLH队列尾部
- 进入自旋状态,不断检查:
- 自己是头节点的下一个节点吗?
- 再次尝试
tryAcquire(1)
能成功吗?
- 如果满足条件,获取锁成功;否则被挂起
释放锁流程🔓:
- 线程调用
release(1)
释放锁 - AQS调用子类的
tryRelease(1)
方法 - 如果释放成功,唤醒队列中的下一个线程
- 被唤醒的线程重新尝试获取锁
3.2 共享模式(如CountDownLatch)
场景:多个线程可以同时访问资源,比如景区限流
获取共享资源流程👥:
- 线程调用
acquireShared(1)
尝试获取 - AQS调用子类的
tryAcquireShared(1)
- 返回值≥0:获取成功
- 返回值<0:获取失败
- 失败后加入队列,等待被唤醒
释放共享资源流程🎉:
- 线程调用
releaseShared(1)
释放资源 - AQS调用子类的
tryReleaseShared(1)
- 如果释放成功,唤醒队列中的等待线程
四、AQS在JDK中的应用实例🌈
4.1 ReentrantLock
ReentrantLock是AQS最典型的应用,它实现了公平锁和非公平锁两种模式:
// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 锁空闲
if (compareAndSetState(0, acquires)) { // CAS抢锁
setExclusiveOwnerThread(current); // 设置持有线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 重入
setState(c + acquires);
return true;
}
return false;
}
4.2 CountDownLatch
CountDownLatch用于让一组线程等待直到计数变为0:
// 共享式获取
public int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 共享式释放
public boolean tryReleaseShared(int releases) {
for (;;) { // 自旋CAS
int c = getState();
if (c == 0) return false;
int nextc = c-1;
if (compareAndSetState(c, nextc)) // CAS减1
return nextc == 0; // 返回是否减到0了
}
}
4.3 Semaphore
Semaphore用于控制同时访问的线程数量:
// 获取许可
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
五、AQS源码深度解析🔬
5.1 节点状态详解
AQS中的Node节点有几种重要状态:
// 取消状态
static final int CANCELLED = 1;
// 等待触发状态(后继节点需要被唤醒)
static final int SIGNAL = -1;
// 等待条件状态
static final int CONDITION = -2;
// 共享模式下传播状态
static final int PROPAGATE = -3;
5.2 入队操作源码
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试在尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // CAS设置尾节点
pred.next = node;
return node;
}
}
enq(node); // 快速添加失败,进入完整入队流程
return node;
}
private Node enq(final Node node) {
for (;;) { // 自旋直到成功
Node t = tail;
if (t == null) { // 队列为空,初始化
if (compareAndSetHead(new Node())) // 设置哑节点
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
5.3 出队与唤醒
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 清除状态
Node s = node.next;
if (s == null || s.waitStatus > 0) { // 下一个节点取消或为空
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0) // 从后向前找第一个有效节点
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒线程
}
六、AQS使用注意事项⚠️
6.1 正确实现模板方法
实现AQS时要注意:
tryAcquire
/tryRelease
要保证线程安全- 状态变更要使用
getState()
/setState()
/compareAndSetState()
- 考虑可重入性
6.2 避免死锁
使用AQS实现的锁要注意:
- 获取锁和释放锁要成对出现
- 在finally块中释放锁
- 避免锁嵌套导致死锁
6.3 性能考量
- 非公平锁通常性能更好
- 尽量减少临界区代码
- 考虑使用读写锁替代独占锁
七、手写一个基于AQS的锁🔐
让我们用AQS实现一个最简单的锁:
public class MyLock {
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
private static class Sync extends AbstractQueuedSynchronizer {
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, 1); // CAS将0改为1
}
protected boolean tryRelease(int arg) {
setState(0); // 直接设置为0
return true;
}
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
}
这个简单的锁虽然功能有限,但已经具备了基本的互斥功能!
八、AQS常见面试题💡
8.1 AQS为什么使用CLH队列?
- 减少争用:CLH队列每个节点只需要轮询前驱的状态
- 内存高效:节点只需要关注前驱,不需要全局同步
- 公平性:天然支持FIFO公平策略
8.2 AQS如何保证可见性?
state
使用volatile修饰- 节点中的
waitStatus
也是volatile的 - 使用Unsafe类提供的CAS操作
8.3 AQS为什么要有头尾指针?
- 头指针:指向当前持有锁的节点
- 尾指针:方便快速入队
- 双向链表:方便取消操作时快速移除节点
九、AQS性能优化技巧🚀
9.1 减少CAS竞争
- 使用多个状态变量分散竞争
- 考虑使用分层锁设计
- 适当增加自旋次数减少上下文切换
9.2 避免不必要的阻塞
- 先快速尝试获取锁
- 使用
tryLock()
替代阻塞获取 - 设置合理的超时时间
9.3 选择合适的同步工具
- 读多写少用
ReadWriteLock
- 并发计数用
CountDownLatch
- 资源池管理用
Semaphore
十、总结🎯
AQS是Java并发包的基石,理解AQS的运作原理:
- 掌握了状态管理的核心思想
- 理解了CLH队列的排队机制
- 学会了模板方法的设计模式应用
下次当你使用ReentrantLock
、CountDownLatch
这些工具时,就能想象到它们背后AQS这位"幕后大佬"是如何工作的了!🤵💼
希望这篇超详细的AQS解析能帮到你!如果有任何问题,欢迎在评论区留言讨论哦~💬😊
记住:并发编程就像管理一个高效的团队,AQS就是那个确保每个人都按规则行事,又能最大限度发挥效率的团队管理者!👨💼👩💼
Happy coding! 🚀💻