线程——AQS详解

    AQS(全称:AbstractQueuedSynchronizer),要探究AQS的原因是,几个常见的同步工具都是基于它实现的(栅栏:CountDownLatch,可重入锁:ReentrantLock,可重入读写锁:ReentrantReadWriteLock)。 AQS本身提供了一个等待队列CLH和一个资源变量(state),通过对资源变量的获取修改释放等操作实现各种同步功能,当多个线程尝试获取资源变量时,如果无法获取到,就会进入等待队列,AQS本身实现了队列和资源变量的维护,包括线程进入队列以及何时被唤醒。
aqs_model

state

    state是AQS维护的一个属性,用volatile修饰,保证其可见性。该属性就是上面说的资源变量,AQS提供了对该变量的操作方法:

getState()
setState()
compareAndSetState()

这三个方法都是由final修饰,不可重构,并且都是原子操作。前两个get、set方法很好理解,第三个compareAndSet基于CAS的原子操作实现的,源码如下:

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

AQS的使用

    基于AQS实现一个同步器,是要继承AbstractQueuedSynchronizer类,并按照自己的需要重构以下几个方法:

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

可以看到其实就是两种类型的获取资源和释放资源,AQS提供两种获取资源的方式,一种是独占方式:比如上面说的可重入锁,一个时刻他只允许一个线程获得共享资源state,另一种是共享方式:比如上面说的栅栏,他是允许多个线程去获取共享资源state的。

原理剖析

资源独占模式

    资源独占模式下,我们要实现锁,都可以通过调用AQS的acquire(int arg)方法实现加锁,该方法表示以独占的方式尝试去获取资源,我们可以在可重入锁ReentrantLock、可重入读写锁ReentrantReadWriteLock、线程池构造器ThreadPoolExecutor中的lock()方法都是通过调用acquire(int arg)实现加锁的。acquire(int arg)源码如下:

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

可以看到acquire中去调用了上面(AQS的使用)写到的这个方法tryAcquire(arg),如果tryAcquire获取到共享资源就直接结束了,否则就会调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)进入等待队列,陷入阻塞。可以看到Node.EXCLUSIVE指明了该节点是一个独占节点。看一下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;
            }
        }
        enq(node);
        return node;
    }

可以看到new了个Node节点,并把当前线程传了进去。然后将这个节点的前一个节点指向原本的尾结点,然后通过compareAndSetTail这个原子操作,替换掉AQS对象中的尾部节点,再把原本的尾部节点的下一个节点指向new出来的这个节点。如果说原本的尾结点为空,或者说compareAndSetTail这个操作失败了(即尾结点已经被其他的线程修改过了)则会走enq(node)这个方法。enq(final Node 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;
                }
            }
        }
    }

enq中是一个自旋锁,如果等待队列为空则通过compareAndSetHead进行自旋(修改头结点),如果不为空则通过compareAndSetTail进行自旋(增加尾结点)。到此完成等待队列的线程节点增加,然后回到外层acquire(int arg)方法,往下继续执行的方法是acquireQueued(final Node node, int arg):

    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;
                }
                //如果当前节点不是头结点,或者头结点获取资源失败
                //判断是否需要park当前线程
                //parkAndCheckInterrupt则是对当前线程进行park
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

我们可以进到shouldParkAfterFailedAcquire(p, node)去看一下:

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

可看到每一个节点都一个状态变量waitStatus,变量定义:

        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

如果节点状态是SIGNAL,则表示节点需要park,如果是大于0即CANCELLED则会执行循环,节点不断往前移,并且当前节点的前一个节点也不断改变(其实就是把那些已经CANCELLED的节点从等待队列中去除掉)。如果既不是SIGNAL也不是CANCELLED,就会把节点的状态通过自旋锁(compareAndSetWaitStatus)改成SIGNAL,然后通过外层的循环进来再次判断节点为SIGNAL,然后park线程。parkAndCheckInterrupt()这个方法则是对当前线程进行park。总之只要队列不是全都为CANCELLED,那一定会park,否则就不断去掉CANCELLED节点,到了头结点,然后循环调tryAcquire()尝试获取共享资源,如果成功了,就会把当前节点设置为头结点,如果失败了就继续循环尝试获取共享资源。

    上面是加锁,然后是释放锁,通过调用AQS的release(int arg) 方法进行释放锁,该方法源码如下:

    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(int arg) 方法来释放锁,如果释放成功,则会从等待队列的头结点开始,唤醒一个最近的线程。

资源独占模式小结

    这种模式核心两个方法acquire和release,需要我们实现的是tryAcquire和tryRelease。整个流程:acquire尝试获取共享资源,如果获取不到就加到等待队列,如果等待队列只剩下head了,就循环去尝试获取共享资源,否则就睡眠。release进行释放共享资源,如果释放成功,就去等待队列中唤醒最靠前的一个节点让他继续去获取共享资源。所以,这么来看的话,AQS实现独占锁是一个公平锁,遵循先来先获取锁的准则。

资源共享模式

    我们熟知的栅栏CountDownLatch是AQS的资源共享模式的典型案例。在CountDownLatch中,我们通过java.util.concurrent.CountDownLatch#await(long, java.util.concurrent.TimeUnit)方法阻塞等待线程,通过java.util.concurrent.CountDownLatch#countDown()来标记一个被等待线程执行结束,当countDown的次数到达阈值,等待线程就会继续往下执行。我们来看一下这两个方法的源码,首先是await方法:

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

sync则是继承自AQS的同步类,我们进入acquireSharedInterruptibly方法:

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

执行了需要我们重写的tryAcquireShared方法,如果共享模式获取资源失败,则会执行doAcquireSharedInterruptibly这个方法,我们看一下这个方法的源码:

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

addWaiter方法创建了一个共享模式的线程节点放入到等待队列,然后进入一个循环,如果前一个节点不是头节点,就park,是头节点就tryAcquireShared尝试获取资源(这里和独占模式是一样的,只是调用的方法一个是tryAcquireShared一个是tryAcquire)。我们来看一下栅栏中tryAcquireShared怎么实现的:

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

如果共享变量等于0,表示获取资源成功,否则失败。而一开始state设置的是阈值,所以等待线程一直无法获取到资源的,所以一直阻塞在那里不断地去获取资源。

    再来看一下countDown这个方法,这个方法直接调用了同步器中的releaseShared方法,源码如下:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

这个方法调用了tryReleaseShared进行释放共享资源,栅栏中的资源释放代码如下:

        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

可以看到和上面的await里的tryAcquireShared相呼应,获取资源是更具state为0进行获取,释放资源时对state进行减一操作,每一个被等待线程进行一次减一,最终等于0,原本的等待线程再去调用tryAcquireShared就可以获取到资源,然后停止等待,往下执行了。资源释放之后调用doReleaseShared方法,这个方法和独占中一样,也是去把阻塞队列中离头节点最近的一个唤醒,让他可以重新执行获取资源的操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值