java的AQS唤醒等待机制
AI-摘要
kerwin GPT
AI初始化中...
介绍自己
生成本文简介
推荐相关文章
前往主页
前往tianli博客
Java的AQS原理以及等待唤醒机制步骤
原理
当抢锁失败时,第一个进入AQS队列的线程节点,会进行初始化,创建一个虚拟头节点,同时将头和尾指针同时指向这个头节点,当后续线程节点进入时,都会连接在头节点之后,形成一个双向链表,其中头指针指向头节点,尾指针指向队列最后一个插入的线程节点。
步骤
当A、B、C三个线程争抢锁时,Java的AQS(AbstractQueuedSynchronizer,抽象队列同步器)内部的实现步骤如下(以非公平锁为例):
1. A线程抢到锁
A线程首先尝试获取锁,调用非公平锁的 lock
方法进行加锁
final void lock() {
//尝试加锁,如果没成功,则调用acquire进行后续抢锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
2. B、C线程试图抢锁
B,C进来也是先调用 lock
方法,进行加锁操作,但是由于A已经加锁,此时会调用acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//tryAcquire底层调用此方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果当前未加锁,则让当前线程占用
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程是持有锁的线程,则进行累加,表明可重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//否则表示抢占失败
return false;
}
3. 抢锁失败,B、C线程进入队列,看是否挂起还是让线程就绪
当B、C线程尝试获取锁时,由于A线程已经持有锁,所以tryAcquire方法会失败。B、C线程在获取锁失败后,会调用addWaiter方法,将自己包装成一个Node节点,并加入到AQS的FIFO队列中。
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)) {
//将之前的最后一个节点的next指向当前线程节点,形成双向链表
pred.next = node;
//返回当前线程节点
return node;
}
}
//如果当前队列没有任何节点,则初始化,并放入当前线程节点,具体逻辑看最前面
enq(node);
return node;
}
4. B、C线程等待唤醒
B、C线程在加入到队列后,会调用acquireQueued方法,此时线程会被阻塞,并等待唤醒。AQS使用LockSupport.park方法来阻塞线程。当一个线程获取锁失败时,它会被加入到AQS队列的末尾,并且调用LockSupport.park方法来阻塞自己。
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; // help GC
//表明抢占成功,不用挂起当前线程
failed = false;
return interrupted;
}
//第一个方法检查当前线程是否应该挂起,如果是,则执行第二个方法,这个方法会将当前线程挂起,并检查线程是否被中断(即其他线程或系统是否发出了中断信号)
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//当前线程撤走,不再竞争
if (failed)
cancelAcquire(node);
}
}
5. A线程释放锁,B、C线程被唤醒
当A线程执行完毕,会调用release方法释放锁,并唤醒队列中的下一个节点对应的线程。AQS会调用LockSupport.unpark方法来唤醒队列中的第一个节点对应的线程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 王德明
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果