共享资源同步器AQS详解

   在ArrayBlockingQueue与LinkedBlockingQueue一文中有提到过一个同步器框架:AbstractQueuedSynchronizer,简称AQS,今天我们来揭开它神秘的面纱。我们知道在ArrayBlockingQueue和LinkedeBlockQueue中,我们间接使用到了AQS,其实它的直接使用是对锁的实现,例如:ReentrantLock、Semaphore、CountDownLatch等。在这里,我们就会有几点疑问了:

1、AQS和锁有什么关系?

2、AQS是怎么实现和线程的交互的?

3、AQS是怎么保证线程安全性的?

下面我们带着这三个疑问,来开启我们的AQS之旅吧!

首先,我们来看一下AQS的框架图,这张图摘自网络,很容易说明它的体系:

通过这张图,我们可以得知,有一个共享资源state,有一个CLH双向链表的队列,为什么叫CLH队列,是由于这是由外国三个人做出来的,他们的名字开头合起来叫CLH,所以以他们的名字命名的,队列中有两个指针,一个头指针head,一个尾指针tail。在进行多个线程在抢占同一个资源的时候,没有获得资源的线程会在一个 队列中进行排队等待,获得资源的线程可以对state资源进行操作,就像我们读书的时候去饭堂打饭,窗口只有一个,优先到达的肯定最先打饭,其他的就只能乖乖在后面排着,这个场景中窗口就是里面的state共享资源,在AQS中,通过volatie关键字定义共享资源,包括state和Node队列,关于volatie关键字,后面也会专门用博客来介绍一下,它可以保证线程之间的可见性,防止指定重排序,但是不能保证例如i++操作的原子性,如何保证state的原子性呢,通过unsafe中的CAS自旋来保证(compareAndSetState方法),首先大家知道这点就可以了。

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;

对于state的访问方式,AQS里面提供了三个方法,一个是获取state的值,一个是给state赋值,还有一个就是通过CAS操作来设置state的值:


    protected final int getState() {
        return state;
    }

    protected final void setState(int newState) {
        state = newState;
    }

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

对于资源的访问,AQS也提供了两种方式,一种是独占(Exclusive),表示同一时刻只能有一个线程执行,例如ReentrantLock,一个是共享(Share),表示同一时刻可以有多个线程同时执行,例如Semaphore、CountDownLatch,也有可能是两者都实现,例如ReentrantReadWriteLock,当然,我们也可以自定义同步器,自定义同步器继承自AQS后只需要实现共享资源state的获取和释放即可,上面的ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock中都有相关的内部类实现了AQS中对于共享资源state的获取和释放,至于线程的等待队列的维护,包括资源获取失败入队,唤醒线程出队等,也就是上图中进入CLH队列中的等待线程,AQS已经帮我们实现好了。

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

查看AQS的源代码,会发现上面的方法并没有定义为一个抽象方法,而是抛出了一个异常:

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

这是因为Doug Lea在做这方面实现的时候,考虑到并不是所有的自定义同步器都需要实现上面每个方法,有的只需要实现其中两个,例如tryAcquire和tryRelease,所以并不是强制性的,用户自己去选择吧。

用ReentrantLock来说,共享资源初始化为state=0,当我们调用它的lock方法的时候,它底层实际上是会执行自己内部实现的tryAcquire方法,将自己变成独占锁,并且让state加1,其他线程在进行tryAcquire的时候就会失败,当然在我们释放锁之前,我们还可以进行上锁,实际上是将state进行累加,这就是ReentrantLock为什么是可重入锁的原因,当然既然调用lock方法,那也应该调用释放锁的方法:unlock,当调用了它的unlock方法的时候,底层也实际上是会执行内部实现的tryRelease方法,将state减1,当state为0的时候,表示该线程释放掉了锁,其他线程就有机会获取到锁了。

上面场景用图示类说明,多个线程想要抢占同一个资源:

例如线程3抢到了资源进行上锁,并将state加1,其他的线程则在CLH队列中等待着,并时刻关注state是不是等于0 ,也就是线程3释放掉锁了木有:

当线程3释放掉锁后,其他线程才有可能再对共享资源上锁:

下面我们来看一下AQS实现的源代码。首先我们来开AQS中独占锁acquire的实现:

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

其中tryAcquire方法是AQS中定义让用户自己实现的方法,这个方法的作用就是尝试去获取共享资源,如果获取成功,则直接返回。接下来我们看addWaiter方法,这个方法的作用就是将一个带有独占锁或共享锁的Node节点强制放入到CLH队尾,意思就是那些个获取共享资源不成功的线程,强制你去排队:

    private Node addWaiter(Node mode) {
	    // 定义一个Node节点,其中mode有两种:EXCLUSIVE(独占)和SHARED(共享)
        Node node = new Node(Thread.currentThread(), mode);
        // 尝试用快速的方式将node放入队尾
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
		// 上面执行不成功,通过enq强制将node放入队尾
        enq(node);
        return node;
    }

我们再来看一下enq方法,这个方法就是通过CAS自旋操作,让其无论如何都要加入到队列中:

    private Node enq(final Node node) {
        for (;;) {//自旋操作,无论如何都要操作成功
            Node t = tail;
            if (t == null) { // 队列为空,则将Node节点设置为头结点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {// 队列不为空,通过CAS自旋让其放入尾部
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

当线程获取共享资源失败,并且加入到CLH队列的尾部了,接下来的acquireQueued方法就是进入等待区休息了,然后等使用共享资源的线程释放锁唤醒自己,自己获取到资源后再进行操作了

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;//这个标记用来表示自己是否拿到共享资源了
        try {
            boolean interrupted = false;//这个标记是等待过程是否被中断过
            for (;;) {
                final Node p = node.predecessor();//拿到该节点的前驱节点
			//如果前驱节点是head,表示自己在CLH队列中处于优先获取资源的节点,那么就去尝试竞争资源
			//head节点释放掉锁,那么就可以唤醒该节点了
                if (p == head && tryAcquire(arg)) {
				// 成功拿到资源,则让head指向该节点
                    setHead(node);
				// 拿到资源后,那么我就去干自己的事情了,此时也就出队了
                    p.next = null; // help GC
                    failed = false;
				//返回等待过程中是否被中断过
                    return interrupted;
                }
			// 没有拿到资源,那就自己休息休息吧,等待着被唤醒
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
					// 等待的过程中被中断的标记
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

其中shouldParkAfterFailedAcquire来检查自己是否真的可以去休息了,比如它的前驱节点是随随便便站着队排一下,那么我就可以不用休息了,知道它找到前面都是正常等待的线程,那么就调用parkAndCheckInterrupt这个方法安心的休息,真正进入等待状态,此时处于waiting状态的线程可以通过unpark或者被interrupt来唤醒,我们来看一下这个方法的大致流程:

1、结点进入队尾后,检查状态,找到安全休息点;
2、调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
3、被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。流程图如下:

acquire方法的总体流程图如下:

看完了acquire方法的实现,接下来我们看release方法的实现,release方法是独占模式下线程释放掉共享资源的入口,如果彻底释放,将state设置为0的时候,则其他线程就会从CLH队列中被唤醒,然后获取到共享资源,源代码如下:

    public final boolean release(int arg) {
	//尝试释放资源
        if (tryRelease(arg)) {
	    //获取到头结点
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒等待队列中的下一个线程
            return true;
        }
        return false;
    }

跟tryAcquire一样,tryRelease方法也是由自定义同步容器实现,主要的操作就是将state-arg,直到它彻底释放资源:state=0

unparkSuccessor方法主要是唤醒CLH等待队列中的没有放弃的处于正常等待的线程:

    /**
     * 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的acquire和release方法大致解析到这里了,还有共享模式acquireShared和releaseShared,后面有时间再介绍,接下来我们看ReentrantLock是怎么和AQS结合起来的,在ReentrantLock中定义了一个抽象的Sync,它继承AQS,实现了里面的tryAcquire和tryRelease方法:

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        abstract void lock();

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
	}

它里面提供了一个抽象的lock方法,用来实现公平锁和非公平锁的实现,我们分别来看一下他们的源代码,公平锁的实现:

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

非公平锁的实现:

	static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

可以看到实现了AQS的ReentrantLock中,tryAcquire方法就是对共享资源state进行加的操作,tryRelease方法就是对state进行减的操作,所以我们在实现AQS的时候tryAcquire和tryRelease、tryAcquireShared和tryReleaseShared都是成对出现的,也就是我们在使用ReentrantLock的lock方法进行上锁后,也应该代码的finally块使用unlock释放掉锁,否则会出现很多不可预知的问题,关于ReentrantLock,我们下篇博客见!!文中有些地方可能写的不是很好,欢迎大家指正。

发布了241 篇原创文章 · 获赞 305 · 访问量 54万+

猜你喜欢

转载自blog.csdn.net/HarderXin/article/details/91856950
今日推荐