Java 并发编程——AQS 源码学习

  • @return the predecessor of this node
    */
    final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
    throw new NullPointerException();
    else
    return p;
    }

Node() { // Used to establish initial head or SHARED marker
}

Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}

Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}

Node 内部类中,声明了 prenext 节点用于队列的连接,同时保存了 waitStatus 状态。

2.2 AQS 类解析

在面向对象的世界中,想要了解一个类有什么特点,就要看它的属性。通过查看源码,我们看到 AQS 类包含:

/**

  • Head of the wait queue, lazily initialized. Except for
  • initialization, it is modified only via method setHead. Note:
  • If head exists, its waitStatus is guaranteed not to be
  • CANCELLED.
    */
    private transient volatile Node head;

/**

  • Tail of the wait queue, lazily initialized. Modified only via
  • method enq to add new wait node.
    */
    private transient volatile Node tail;

/**

  • The synchronization state.
    */
    private volatile int state;

  • 前驱头节点 - head

  • 后驱尾节点 - tail

  • 同步器状态 - state

AQS 基于 FIFO 队列,接下来就依照 acquire-releaseacquireShared-releaseShared 的次序来分析入队和出队。

2.2.1. acquire(int)

此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源成功,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是 lock() 的语义,当然不仅仅只限于 lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是 acquire 源码:

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

acquire 方法中调用了 tryAcquire()acquireQueued()addWaiter() 三个方法。

首先通过tryAcquire方法尝试申请独占锁。如果获取成功则返回 true,否则返回 false

protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

源码中直接 throw 一个异常。结合我们前面自定义锁的知识,AQS 只是一个框架,具体资源获取和释放方式交由自定义同步器实现。AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过 stateget/set/CAS)!!!至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。

这里之所以没有定义成 abstract,是因为独占模式下只用实现 tryAcquire-tryRelease,而共享模式下只用实现 tryAcquireShared-tryReleaseShared。如果都定义成 abstract,那么每个模式也要去实现另一模式下的接口。说到底,Doug Lea 还是站在咱们开发者的角度,尽量减少不必要的工作量。

1. addWaiter()

接着就是 addWaiter() 方法用于将当前线程添加到等待队列的队尾,并返回当前线程所在的节点

private Node addWaiter(Node mode) {
//以给定的Node节点模式构建当前线程的Node节点,在acquire方法中传入的是EXCLUSIVE独占式节点
Node node = new Node(Thread.currentThread(), mode);
// 将尾节点进行保存
Node pred = tail;
if (pred != null) {//如果尾节点不为null
//将尾节点设置尾新节点的prev节点
node.prev = pred;
if (compareAndSetTail(pred, node)) {//通过CAS保证,确保节点能够被线程安全的添加
//将当前界定指向前驱的next节点
pred.next = node;
return node;
}
}
//如果尾节点为null,则通过enq进行入队
enq(node);
return node;
}

//同步器通过死循环的方式来保证节点的正确添加,在“死循环” 中通过CAS将节点设置成为尾节点之后,
//当前线程才能从该方法中返回,否则当前线程不断的尝试设置。
private Node enq(final Node node) {
//CAS"自旋",直到成功加入队尾
for (;😉 {
//尾节点临时存储
Node t = tail;
if (t == null) { // Must initialize
//如果tail为null,则将
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

addWaiter(Node node) 方法中,将当前线程节点添加到等待队列中。

enq.png

2. acquireQueued()

acquireQueued 在队列中的线程获取锁

/**

  • Acquires in exclusive uninterruptible mode for thread already in
  • queue. Used by condition wait methods as well as acquire.
  • @param node the node
  • @param arg the acquire argument
  • @return {@code true} if interrupted while waiting
  • acquireQueued方法当前线程在死循环中获取同步状态,而只有前驱节点是头节点才能尝试获取同步状态(锁)( p == head && tryAcquire(arg))
  • 原因是:1.头结点是成功获取同步状态(锁)的节点,而头节点的线程释放了同步状态以后,将会唤醒其后继节点,后继节点的线程被唤醒后要检查自己的前驱节点是否为头结点。
    
  •       2.维护同步队列的FIFO原则,节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说是每个线程)都在自省的观察。
    

*/
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) &&
//如果需要挂起,借助JUC包下面的LockSupport类的静态方法park挂起当前线程,直到被唤醒
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果有异常
if (failed)
//取消请求,将当前节点从队列中移除
cancelAcquire(node);
}
}

通过 addWaiter 方法添加到等待队列中后,在通过 acquireQueued 方法进行锁的获取。

大体流程如下:

  • tryAcquire() 尝试直接去获取资源,如果成功则直接返回;
  • addWaiter() 将该线程加入等待队列的尾部,并标记为独占模式;
  • acquireQueued() 使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回 true,否则返回 false
  • 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断 selfInterrupt(),将中断补上。
3. 独占式锁获取流程

调用同步器的 acquire(int arg) 方法可以获取同步状态,该方法对中断不敏感,即线程获取同步状态失败后进入同步队列,后续对线程进行中断操作时,线程不会从同步队列中移除。获取流程:

  1. 当前线程通过 tryAcquire() 方法尝试获取锁,成功则直接返回,失败则进入队列排队等待,通过 CAS 获取同步状态。
  2. 如果尝试获取锁失败的话,构造同步节点(独占式的 Node.EXCLUSIVE),通过 addWaiter(Node node,int args) 方法,将节点加入到同步队列的队列尾部。
  3. 最后调用 acquireQueued(final Node node, int args) 方法,使该节点以死循环的方式获取同步状态,如果获取不到,则阻塞节点中的线程。acquireQueued 方法当前线程在死循环中获取同步状态,而只有前驱节点是头节点的时候才能尝试获取锁(同步状态)( p == head && tryAcquire(arg))。

lock.png

2.2.2 release(int) 独占锁的释放

AQS 中通过 release 方法进行锁的释放。

public final boolean release(int arg) {
//调用tryRelease方法释放
if (tryRelease(arg)) {//如果释放成功
Node h = head;
//如果头节点不为null,并且头结点的waitStatus值不为0,即有状态
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
//没有释放成功返回false
return false;
}

// tryRelease() 尝试释放当前线程的同步状态(锁)
protected final boolean tryRelease(int releases) {
//c为释放后的同步状态
int c = getState() - releases;
//判断当前释放锁的线程是否为获取到锁(同步状态)的线程,不是抛出异常(非法监视器状态异常)
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果锁(同步状态)已经被当前线程彻底释放,则设置锁的持有者为null,同步状态(锁)变的可获取
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState©;
return free;
}

private void unparkSuccessor(Node node) {
/*

  • If status is negative (i.e., possibly needing signal) try
  • to clear in anticipation of signalling. It is OK if this
  • fails or if status is changed by waiting thread.
    */
    int ws = node.waitStatus;
    if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);

/*

  • Thread to unpark is held in successor, which is normally
  • just the next node. But if cancelled or apparently null,
  • traverse backwards from tail to find the actual
  • non-cancelled successor.
    */
    Node s = node.next;
    //从队列尾部开始往前去找最前面的一个waitStatus小于0的节点。
    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);
    }

release() 是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即 state=0),它会唤醒等待队列里的其他线程来获取资源。

2.2.3 acquireShared(int)

此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是 acquireShared() 的源码:

public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//加入队列尾部
boolean failed = true;//是否成功标志
try {
boolean interrupted = false;//等待过程中是否被中断过的标志
for (;😉 {
final Node p = node.predecessor();//前驱
if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//成功
setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted)//如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
}

//判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

这里 tryAcquireShared()依然需要自定义同步器去实现。但是 AQS 已经把其返回值的语义定义好了:负值代表获取失败;0 代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里 acquireShared() 的流程就是:

  1. tryAcquireShared() 尝试获取资源,成功则直接返回;
  2. 失败则通过 doAcquireShared() 进入等待队列 park(),直到被 unpark()/interrupt() 并成功获取到资源才返回。整个等待过程也是忽略中断的。

doAcquireShared(int) 此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。

2.2.4 releaseShared()

releaseShared() 是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。

public final boolean releaseShared(int arg) {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

分享一些资料给大家,我觉得这些都是很有用的东西,大家也可以跟着来学习,查漏补缺。

《Java高级面试》

《Java高级架构知识》

《算法知识》

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

最后

分享一些资料给大家,我觉得这些都是很有用的东西,大家也可以跟着来学习,查漏补缺。

《Java高级面试》

[外链图片转存中…(img-E1ZK2ced-1713112411394)]

《Java高级架构知识》

[外链图片转存中…(img-XpzLHBqX-1713112411394)]

《算法知识》

[外链图片转存中…(img-wze8vZjT-1713112411394)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值