JUC学习笔记:AQS原理

AQS框架

首先对于syncronized,锁对象的对象头指向了一个管程,由管程负责维护EntryListWaitSet的队列。
同样的,对于我们自定义的锁,除了线程安全地获取和释放锁之外,也需要做维护这些等待队列的工作。而AQS提供的就是一个这样的维护等待队列的机制,用户只需要关注于自己的锁的释放和获取的逻辑就可以了。这就是AQS的基本功能。
AQS内部维护了一个volatile修饰的state表示锁的占有状态,同时维护了一个线程等待的CLH队列。
在这里插入图片描述
AQS将资源分为独占和共享的,因此自定义同步器只需要实现对应的tryAcquire-tryRelease和tryAcquireShared-tryReleaseShared即可

源码

结点

节点设置了头尾结点指针、结点对应线程、结点状态等信息,结点状态分成以下几个

  • CANCLED(1):结点取消调度,timeout或中断的话会出发变更成此状态,进入此状态后结点不在变化
  • (0):新结点进入队的初始状态
  • SIGNAL(1):后续有结点在等待本结点的唤醒
  • CONDITION(2):该结点等待在CONDITION上,有其他线程唤醒时该线程将从等待队列转移到同步队列获取同步锁
  • PROPAGATE(3):共享模式下不仅会唤醒后继结点,还会唤醒后继结点的后继结点

独占模式

acquire

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

首先执行tryAcquire来尝试获取同步锁,这个方法是需要调用者自行定义的,AQS中只抛出了一个异常,之所以没用抽象方法是因为除了独占模式还有共享模式,使用抽象方法的话无论哪种模式都要实现另一种模式下的抽象方法,所以为了方便这里没有定义成一个抽象的方法

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

其次将Node添加到等待队列当中

    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)) {
                pred.next = node;
                return node;
            }
        }
        //按部就班添加到尾部结点
        enq(node);
        return node;
    }
    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;
                }
            }
        }
    }

在线程进入队列之后就需要排队获取同步锁了,这就是上面的acquireQueued()方法,注意在等待的过程中如果超时或者被中断了会执行cancelAcquire方法放弃等待。该方法的一次循环是执行尝试获得锁或者暂停本线程

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);
        }
    }

进入等待的shouldParkAfterFailedAcquire(p, node)方法主要做的就是找到前面第一个没有放弃获取锁、也就是状态小于0的结点,将这个状态小于零的结点置为-1,使其释放锁之后通知当前结点

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

再告知前面的结点通知自己之后,该线程就可以暂停了

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
总结

上锁经历了这样一个过程

  1. 尝试获取锁
  2. 建立本线程的结点
  3. 如果该结点是哑元后的第一个结点,尝试获得同步锁,失败的话结点进入队尾
  4. 通知前面的结点在释放锁之后通知本结点
  5. 本线程park暂停

release

唤醒过程的代码如下,首先尝试释放同步锁,如果同步锁被彻底释放,会唤醒等待队列中的线程使其尝试获取锁(见acquire的代码)

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

同样地,tryRelease逻辑由调用者编写,返回资源释放是否成功的布尔值

    /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    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;
        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);
    }

对于结点的唤醒是从后往前找的,因为在AQS的代码中入队和取消排队的prev炼是用CAS操作保证强一致性的,而next链并非强一致的

共享模式

acquireShared

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

tryAcquireShared方法同样是用户自定义的逻辑,尝试获取资源并返回一个整型值,非负数表示剩余资源数量,负数表示失败,doAcquireShared也是类似acquire的入队流程

    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) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

值得注意的是,尝试获取到资源之后,除了唤醒自己之外,还会唤醒自己的下一个结点尝试获取资源,这是与独占不同的地方,独占是释放资源才会唤醒后继的线程

releaseShared

releaseShared也跟release相似,每一个线程释放资源后都会唤醒队列中的线程

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

应用

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

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

CountDownLatchReentranLock等的实现都基于AQS

ReentrantLock

  • ReentrantLock有公平的和非公平的两种基于AQS的同步器的实现,不同在于是否在竞争锁之前检查等待队列的状态
  • ReentrnatLock可重入是基于判断锁的Owner是否是当前线程,如果是的话计数增加,同理释放的时候计数减少,计数为0的时候就证明锁完全释放了
  • ReentrantLock的可打断:AQS的实现中也有一个打断标记,不过只是将这个打断标记返回,而可打断逻辑当中是在打断的时候抛出了异常
  • ReentrantLock还维护了ConditionObject对象,内部也维护了一个队列,当唤醒该Condition的时候会将队列当中的结点添加到AQS维护的等待队列当中
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值