Java并发AQS队列同步器源码学习笔记

如果你恰巧翻到了这篇文章,建议可以看这两篇文章,写的很好。Java并发之AQS详解(JDK)ReetrantLock手撕AQS

前言

锁和AQS的关系

在讲队列同步器AbtstractQueuedSynchronizer(AQS,下文简称为同步器)之前,先了解锁和它的关系。AQS是实现锁或者说很多同步组件的关键,我们可以从源码中看到很多类继承了AQS,例如重入锁,读写锁等等。换句话说,锁是面向使用者,而同步器是实现锁的关键组件。

同步器的接口

同步器的设计是基于模板方法模式的,使用者(也就是上面截图中的这些类,例如FairSync,NonFireSync等)需要继承并重写指定的方法,随后将重写的方法组装到自定义的同步组件实现中。

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以上5个方法为子类可以重写的方法,摘抄自Java并发之AQS详解。除了可以重写的方法,同步队列AQS中会提供一些模板方法,这些方法可以直接被其子类调用来实现自定义同步组件。例如独占是获取同步状态的acquire(),独占式释放同步状态release()等等。本文只分析acquire()和release()的相关方法。

acquire()方法

这是AQS的最重要的方法之一

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

此方法是独占式获取同步状态,如果当前线程获取同步状态成功,则返回,如果不成功,将会进行同步队列等待。那么如何具体实现呢?

  1. tryAcquire(int)独占式获取同步状态,如果当前线程获取同步状态成功,则返回。
  2. 如果不成功,使用addWaiter(Node)方法将当前线程构造成节点加入到同步队列尾部。
  3. 最后调用acquireQueued(Node, int)方法使得该节点以“死循环”的方式获得同步状态。如果获得不到,则阻塞当前节点中的线程,那么被阻塞谁来唤醒呢,唤醒是由前驱节点出队列或者阻塞线程被中断来实现。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断

tryAcquire(int arg)

这个方法就是用来独占式的获取当前线程的同步状态,如果成功返回true,失败返回false,但是这个方法体中怎么直接抛出异常了呢?什么鬼?这就是上面提到的同步器需要继承自己的子类所要实现的方法。说白了就是谁要用谁自己实现,AQS只是提供一个框架和一些必要的方法。 

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

addWaiter(Node mode)

这个方法是将当前线程构造为节点,添加到等待队列尾部。

private Node addWaiter(Node mode) {
    // 将当前线程构造成节点,mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 尝试快速添加到队尾
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // 确保节点可以被添加到队尾
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果失败则进入enq方法,通过“死循环”的方式确保并发的节点可以成功添加
    enq(node);
    return node;
}

直接看代码, 这里稍微提一下Node这个静态内部类的数据结构以及属性。

Node

AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态。

static final class Node {
        // 共享式模式
        static final Node SHARED = new Node();
        // 独占式模式
        static final Node EXCLUSIVE = null;

        // 表示同步队列中等待线程被打断或者等待超时,需要从同步队列中取消
        static final int CANCELLED =  1;
        // 在后继节点处于等待状态,而当前节点线程释放了同步状态或者被取消,将会通知后继节点,使得后集结点得以运行
        static final int SIGNAL    = -1;
        // 这个是和Condittion有关,
        static final int CONDITION = -2;
        // 表示下一次共享式同步状态将会无条件的传播下去
        static final int PROPAGATE = -3;

        // 等待状态,初始状态为0,其余状态就是上面提到的1,-1等等
        volatile int waitStatus;

        // 节点的前节点
        volatile Node prev;

        // 节点的后继节点
        volatile Node next;

        // 线程值
        volatile Thread thread;

        // 等待队列中的后继节点。如果当前节点时共享的,那么该字段是SHARED常量
        Node nextWaiter;
        ...
    }

接着我们看enq()方法

enq(final Node node)

enq方法将并行添加的节点请求通过CAS变得串行化,实现节点的队尾插入。

private Node enq(final Node node) {
    // “死循环”将node插入队尾
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 通过CAS将节点放入队尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

接着我们来看acquireQueued(final Node node, int arg)方法

acquireQueued(final Node node, int arg)

到了这一步说明获取同步状态失败,当前线程被构造成节点后已经添加到队列尾部,进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,通过for的死循环,把等待队列中的节点状态不是大于0的无效状态的节点的节点状态置为SIGNAL,让它们进入WAITING状态,而只有当头结点获得同步状态后移除同步队列,将后续节点设置为头结点,继续进行同步状态的获取。换句话说就是等待队列中的节点除了头结点在获取同步状态,其余的除了节点状态大于0的都被park,都处于WAITING状态。

    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节点已经获得同步状态,将后节点设置为null时是为了垃圾回收
                    p.next = null;
                    failed = false;
                    // 返回是否被打断
                    return interrupted;
                }
                // 线程阻塞,需要其他线程进行唤醒
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
            }
        } finally {
            if (failed)
                // 如果出现异常或者出现中断,将线程的状态改为CANCELLED
                cancelAcquire(node);
        }
    }

在acquireQueued(final Node node, int arg)方法中还主要涉及一下两个方法,继续来看

shouldParkAfterFailedAcquire(Node, Node)

这个方法主要是检查和更新节点状态,如果线程阻塞则返回true,就是将不能够获得同步状态的节点都让他们等待,除非被唤醒或者打断。

    // 检查和更新未能获取的节点的状态。如果线程阻塞,返回true。
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;// 拿到前驱节点状态
        if (ws == Node.SIGNAL)
            // 如果前驱节点状态为SIGNAL,表示前趋结点会通知它,那么它可以放心大胆地挂起了
            return true;
        if (ws > 0) {
            // 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 如果前驱正常,那就把前驱的状态设置成SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt()

这个方法就是让线程进行等待,park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。

    // 如果被打断返回true
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//调用park()使线程进入waiting状态
        return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
    }

 acquireQueued(final Node node, int arg)方法总结,重要!

  1. 该方法传入一个node节点,先判断这个节点是不是头结点,也就是说如果当前队列是空队列,现在就只有这么一个节点(其实还有一个null节点,它的后序节点就是刚添加进来的这个节点),如果是头结点,那么调用自定义同步器重写的tryAcquire()方法进行独占式同步队列的获取。如果获取失败继续获取,这是一个for的死循环。那么如果不是空队列,添加到这个同步队列中,假设队列中已经有5个节点,那么现在这个刚添加的节点进入此方法的for死循环,进行判断是它的前驱节点是否为头结点,明显不是,因为队列中它的前面还有5个节点。
  2. 既然前驱节点不是头结点,那么进入阻塞方法shouldParkAfterFailedAcquire(Node, Node)方法,它会节点判断是否要让这个节点中的线程阻塞,传入的第一个参数是当前节点的前驱节点,第二个参数是当前节点,如果前驱节点的状态是SIGNAL,那么就返回true,接着让接下来的parkAndCheckInterrupt()方法真正进行park,如果状态>0,那么就跳过,找状态不大于0的有效状态,如果既不是SIGNAL,也不是>0的情况,那么就将前驱节点的状态置位SIGNAL,其实正常情况下,按照假设的情况,它是第六个节点,那么它前面的节点状态要么是无效状态,要么都已经置为SIGNAL,除了头结点正在获取同步状态,剩下的节点中的线程都处于WATTING状态。因为这里是for的死循环,假设的这个节点是第六个节点,for循环一次,要么第五个节点已经是SIGNAL,要么给它置为SIGANL,如果不是SIGANL,这次置为SIGANL后,那么下一次添加了第七个节点,直接判断得出已经是SIGANL了,已经进入阻塞状态了,不用再进行CAS进行SIGNAL信号的赋值了。
  3. 既然已经阻塞了,那么进行了parkAndCheckInterrupt()方法体,直接将当前线程park,让其进入等待状态。直到被unpark()或interrupt()唤醒。
  4. 在唤醒后,继续进入for循环判断自己是否有条件获取同步状态,如果有,那么将后序节点置为头结点,并返回自己是否被打断过。如果不能获得同步状态,那么继续进入阻塞状态。

require()方法总结

总结一下独占式获得同步状态的过程:

  1. 调用自定义同步器重写的tryAcquire()方法来独占式获取同步状态,如果获取成功则返回。
  2. 如果失败,将当前线程构造为节点,调用addWaiter()方法添加到等待队列尾部。
  3. acquireQueued()方法,使得当前节点的线程排队等待,处于WATTING状态,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

说完了独占式获取同步状态,再说一下独占式释放线程

release(int arg)

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            // 找到头结点
            Node h = head;
            // 头结点不为null,而且头结点状态不是初始值0,也就是其他节点可以获取同步状态
            if (h != null && h.waitStatus != 0)
                // 唤醒node的后继节点(如果存在的话)。
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

在这个方法中我们可以看到是通过tryRelease(arg)的返回值来判断是否将同步状态释放,这个方法也是自定义同步器需要继承重写的方法,代码如下

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

接着我们可以看到还有个unparkSuccessor(Node node)方法,它唤醒队列中等待的下一个线程

unparkSuccessor(Node node)

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;//拿到当前节点状态
        if (ws < 0)
            // 置零当前节点状态,可能会失败
            compareAndSetWaitStatus(node, ws, 0);
        // 后继节点
        Node s = node.next;
        // 如果为null或者已取消
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从尾部向前遍历
            for (Node t = tail; t != null && t != node; t = t.prev)
                // 如果节点状态<=0,是有效状态
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);// 唤醒线程
    }

这个方法核心就是unpark唤醒了队列中最前面等待的线程。

 

 

 

 

 

 

 

参考文献

[1]https://www.cnblogs.com/waterystone/p/4920797.html

[2]https://www.jianshu.com/p/e4301229f59e

[3]https://blog.csdn.net/novelly/article/details/15856563

[4]https://zhuanlan.zhihu.com/p/54297968

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值