AQS——同步队列独享模式

AQS内部的队列

AQS内部通过链表来维护了一个同步队列与等待队列,Node类代码如下:

static final class Node {
        // 共享状态的节点
        static final Node SHARED = new Node();
        // 独占状态的节点
        static final Node EXCLUSIVE = null;

        // 当前等待的线程被取消或被中断
        static final int CANCELLED =  1;
        // 当前节点为SIGNAL时,后继节点才能够被挂起
        static final int SIGNAL    = -1;
        // 表明处于等待队列的节点
        static final int CONDITION = -2;
        // 共享节点必须无条件的传递给下一个节点
        static final int PROPAGATE = -3;
        // 0 初始化状态

        // 当前节点状态
        volatile int waitStatus;

        // 前一个节点
        volatile Node prev;

        // 下一个节点
        volatile Node next;

        // 队列节点中的线程
        volatile Thread thread;

        // 当waitStatus为CONDITION时,表明处于等待队列的下一个节点
        // 当nextWaiter指向SHARED时,表明处于共享状态
        // 当同步队列实现为排他时,nextWaiter都为null
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @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;
        }
    }

维护的同步队列与等待队列如下:

同步队列中的节点是获取资源失败时加入到该队列中的节点,该节点初始状态为0。该队列一般有两种模式,独占资源或者共享资源。

等待队列通过实现Condition接口中的方法,模拟wait()/notify(),当线程获取到锁时,调用await()方法时,会由同步队列中移入等待队列并等候唤醒;当调用signal()方法时,在doSignal()内部调用transferFoSignal()将节点放置回同步队列末尾,

final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        // p 是 node的前驱
        Node p = enq(node);
        int ws = p.waitStatus;
        // 如果p被取消了或者p 设置为SIGNAL失败(node不可能进入阻塞状态而被唤醒)
        // 则唤醒线程重新进行资源的获取
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

下面聊聊独占模式的获取/释放与共享模式的获取/释放:

独占模式acquire()与release()

从独占模式的acquire()入手,

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

当线程尝试获取资源时,tryAquire()成功则不用加入同步队列立即返回。否则将执行入队操作,首先调用addWaiter():

private Node addWaiter(Node mode) {
        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;
            }
        }
        // 否则生成新的空节点,并让head、tail指向该节点
        enq(node);
        return node;
    }

然后会执行acquireQueued(),

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;
                    // 执行过程中有中断,那么该线程最后会响应中断
                    // 即当前线程不会继续执行同步区域代码,而直接进入RUNNABLE状态
                    return interrupted;
                }
                // 如果前一个节点不是头节点或者重新获取资源失败
                // 判断前一个节点p是否是SIGNAL状态的
                // 前一个节点是SIGNAL状态才会执行parkAndCheckInterrupt()挂起当前线程
                // parkAndCheckInterrupt()执行过程中会检测是否有中断操作
                // 如果有中断操作,将interrupted置为true
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 最终没有获取到资源
            // 将当前线程取消并出队
            if (failed)
                cancelAcquire(node);
        }
    }

来看看shouldParkAfterFailedAcquire()内部实现,

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 前一个节点是SIGNAL
            // 返回true
            return true;
        if (ws > 0) {
            // 如果前一个节点被取消
            // 将前一个节点移除同步队列
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 状态为0 或 PROPAGATE时
            // 将前一个节点状态转换为SIGNAL
            // 以便下一次执行获取资源操作成功后可以挂起当前节点线程
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

总结一下acquire()的流程:

  1. 首先通过tryAcquire()进行资源获取,如果获取成功不进入同步队列直接进入下一步
  2. 获取失败则调用addWaiter(),构造当前线程的同步节点并入队
    1. addWaiter()入队分两种情况
    2. 当前队列中有节点(即可以找到原同步队列最后一个节点),则将新构造的节点原子的设置为尾节点
    3. 当前队列中无节点执行enq()方法,原子的设置头节点,并将尾节点指向头节点所在内存区域
  3. 然后执行acquireQueued()方法
    1. 判断当前节点前一个节点是否为头节点
      1. 不是,调用shouldParkAfterFailedAcquire()
        1. 判断前一个节点waitStatus是否为SIGNAL;
          1. 是 立即返回true
          2. 不是  如果前一个节点为null,将前一个节点的前一个节点指向当前节点(将取消获取资源的节点出队)
          3. 否则将执行原子操作将前一个节点的状态替换为SIGNAL并返回false
        2. 如果 shouldParkAfterFailedAcquire()返回false继续执行3.1
        3. 如果shouldParkAfterFailedAcquire()返回true则执行parkAndCheckInterrupt()操作挂起当前线程
      2. 是头节点,执行tryAcquire()操作又进行一次资源获取操作
        1. 成功获取资源
          1. 将头节点指向当前节点
          2. 线程执行后续操作
        2. 失败则执行3.1.1的调用shouldParkAfterFailedAcquire()

前面的acquire()方法里面都只是在park一个线程,即只有入队,那么当获取到资源的线程利用完后怎么调度队列中的节点进行资源的获取呢?接下来继续分析release()这个方法。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

可以看出release()方法中,调用tryRelease()判断资源是否被释放,如果资源被释放了则执行unparkSuccessor(),唤醒被挂起的节点(头节点的下一个节点)。

当同步队列中头节点的下一个节点被唤醒时,会继续之前的acqireQueued()方法,此时如果没有额外的竞争,那么tryAcquire()方法调用成功,下一个节点成功获取资源,并将头节点指向下一个节点。

通过分析可以看出,独享模式中,能够允许同时获取资源的线程数为一,即头节点的下一个节点被唤醒并得到资源时,该节点中的线程将被释放去执行之后的代码,并将头节点指向该节点的内存区域。

独享模式流程

  1. 假定最初共享资源被获取
  2. 此时线程A想要获取资源,则会将同步队列初始化并将节点A加入到同步队列
  3. 资源还在被持有的同时,线程B也来获取资源,也会加入到同步队列
  4. 当资源被释放时,调用tryRelease()方法成功时,将会唤醒阻塞在同步队列的第一个节点即A,A节点中的线程将会执行接下来的代码                              
  5. 当资源再次被释放时,B将重复类似第四步的操作       
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值