AQS的主要方法源码解析

网友投稿 649 2022-05-29

AQS的主要方法源码解析

AQS的主要方法源码解析

AQS的设计是基于模板方法模式的,它有一些方法必须要子类去实现的,它们主要有:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

这些方法虽然都是protected方法,但是它们并没有在AQS具体实现,而是直接抛出异常(这里不使用抽象方法的目的是:避免强迫子类中把所有的抽象方法都实现一遍,减少无用功,这样子类只需要实现自己关心的抽象方法即可,比如 Semaphore 只需要实现 tryAcquire 方法而不用实现其余不需要用到的模版方法):

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

而AQS实现了一系列主要的逻辑。下面我们从源码来分析一下获取和释放资源的主要逻辑:

获取资源

获取资源的入口是acquire(int arg)方法。arg是要获取的资源的个数,在独占模式下始终为1。我们先来看看这个方法的逻辑:

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

首先调用tryAcquire(arg)尝试去获取资源。前面提到了这个方法是在子类具体实现的。

如果获取资源失败,就通过addWaiter(Node.EXCLUSIVE)方法把这个线程插入到等待队列中。其中传入的参数代表要插入的Node是独占式的。这个方法的具体实现:

private Node addWaiter(Node mode) { // 生成该线程对应的Node节点 Node node = new Node(Thread.currentThread(), mode); // 将Node插入队列中 Node pred = tail; if (pred != null) { node.prev = pred; // 使用CAS尝试,如果成功就返回 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 如果等待队列为空或者上述CAS失败,再自旋CAS插入 enq(node); return node; } // 自旋CAS插入等待队列 private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }

上面的两个函数比较好理解,就是在队列的尾部插入新的Node节点,但是需要注意的是由于AQS中会存在多个线程同时争夺资源的情况,因此肯定会出现多个线程同时插入节点的操作,在这里是通过CAS自旋的方式保证了操作的线程安全性。

OK,现在回到最开始的aquire(int arg)方法。现在通过addWaiter方法,已经把一个Node放到等待队列尾部了。而处于等待队列的结点是从头结点一个一个去获取资源的。具体的实现我们来看看acquireQueued方法

final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // 自旋 for (;;) { final Node p = node.predecessor(); // 如果node的前驱结点p是head,表示node是第二个结点,就可以尝试去获取资源了 if (p == head && tryAcquire(arg)) { // 拿到资源后,将head指向该结点。 // 所以head所指的结点,就是当前获取到资源的那个结点或null。 setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 如果自己可以休息了,就进入waiting状态,直到被unpark() if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

这里parkAndCheckInterrupt方法内部使用到了LockSupport.park(this),顺便简单介绍一下park。

LockSupport类是Java 6 引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:

park(boolean isAbsolute, long time):阻塞当前线程

unpark(Thread jthread):使给定的线程停止阻塞

所以结点进入等待队列后,是调用park使它进入阻塞状态的。只有头结点的线程是处于活跃状态的。

当然,获取资源的方法除了acquire外,还有以下三个:

acquireInterruptibly:申请可中断的资源(独占模式)

acquireShared:申请共享模式的资源

acquireSharedInterruptibly:申请可中断的资源(共享模式)

任务调度

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:【云端大事件】--“共享、开放、融合——大数据开启智慧政务新时代”研讨会在沈阳隆重召开
下一篇:【高并发】为什么局部变量是线程安全的?
相关文章