java8 多线程并发之AQS详解

AQS是类AbstractQueuedSynchronizer的缩写,是一个队列式的同步器,功能是用于限制多线程对资源的访问,在java中,很多与锁或者同步相关的类都依赖AQS,比如ReentrantLock、Semaphore等。对于如何访问共享资源和独占资源,它提供了一个基本的框架。

一、基本原理

AQS内部有一个类似于锁或者通行证的属性字段,线程每次访问资源前,都需要调用AQS中的方法获得这个锁,如果没有拿到锁,则将该线程放入一个等待队列的尾部,并置线程状态为waiting,线程进入休眠状态。
资源访问结束后,线程需要调用相应的方法释放锁,同时从等待队列中找到第一个进入队列的线程将其唤醒,该被唤起的线程就开始尝试获得锁,如果能拿到锁,就开始运行,如果拿不到就继续等待。
下面重点介绍一下等待队列。
AQS将没有拿到锁的线程都放入一个等待队列中,这个等待队列是按照申请锁的先后顺序排列的,遵循先进先出。AQS的等待队列是CLH锁队列(CLH lock queue)的一个变种,CLH是三个人的名字缩写:Craig, Landin和Hagersten。CLH锁队列使用CAS和自旋锁,既兼顾了性能,又可以不让线程长期保持“饥饿”状态。CLH锁队列如下图:
在这里插入图片描述
该队列是一个双向队列,除了头结点之外的其他节点都代表了一个等待锁的线程,头结点在大部分情况下都代表一个持有锁且正在运行的线程。

二、源码详解

1、准备知识

在AbstractQueuedSynchronizer中,使用内部类Node表示AQS队列中一个节点:

	//代码有删减
   static final class Node {
        //节点状态
        volatile int waitStatus;
		//指向队列的前一个节点
        volatile Node prev;
		//指向队列的后一个节点
        volatile Node next;
		//记录当前线程
        volatile Thread thread;
		
        Node nextWaiter;
		//获得队列的前一个节点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
    }

每个Node对象都一个Thread属性,因此每个Node节点都代表了一个等待锁的线程。
属性waitStatus表示节点状态,它有如下几个取值:

  1. CANCELLED(1):表示当前结点已取消申请锁。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  2. SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  3. CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  4. PROPAGATE(-3):共享模式下,当前驱节点申请到锁或者释放锁时都可以唤醒后继节点,后继节点还可以继续唤醒它的后继节点,这样依次往后传播,该值只在共享模式下doReleaseShared()方法中使用。
  5. 0:新结点入队时的默认状态。

从取值上可以看到,当waitStatus>0时,只有一个CANCELLED状态,在代码里面有很多地方使用waitStatus>0判断节点状态是否正常。

在AbstractQueuedSynchronizer中,锁使用属性state表示:

private volatile int state;

严格来说,state不是锁,在不同是子类里面,它表示不同的含义,比如子类ReentrantLock,state初始值为0,线程如果加锁成功,那么state加1,线程解锁的话,state减1,当state=0时,表示线程不在持有锁;在子类Semaphore,state初始为信号量的个数,线程每获得一次信号量,state就减去相应的值,当state=0时,便阻塞线程。

AbstractQueuedSynchronizer名字中带有Abstract表示该类是一个抽象类,该类中并没有抽象方法,但是根据子类的应用场景,有几个方法需要重写:

	//独占模式下,尝试加锁,如果成功,返回true,失败返回false
	protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
	//独占模式下,尝试释放锁,如果全部释放,返回true,否则返回false
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
	//共享模式下,尝试加锁,返回负数表示加锁失败;
	//0代表获取成功,但没有剩余锁;正数表示获取成功,还有剩余锁,其他线程还可以去获取。
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
	//共享模式下,尝试释放锁,如果全部释放,返回true,否则返回false
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
	//检查当前线程是否处于独占模式,如果是,返回true。只有用到Condition才需要去实现它。
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

AQS中将需要覆盖的方法定义为protected,且默认实现都是抛异常的。这些方法分为了两种模式:共享模式和独占模式。

  • 共享模式指的是多个线程共享资源,比如类Semaphore,允许多个线程同时访问资源,可以认为有多把锁,每个线程可以申请一个或多个,凡是申请到的都可以访问资源;
  • 独占模式指的是每个资源同时只能一个线程访问,比如ReentrantLock,如果一个线程加锁成功,其他的线程禁止访问。

如果应用场景是线程独占资源,那么子类只实现tryAcquire()/tryRelease()两个方法即可,如果是共享资源,那么子类实现tryAcquireShared()/tryReleaseShared()。
通过这些方法的含义,可以知道子类里面只需要考虑如何加锁和释放锁,至于AQS队列的维护和选择哪个线程尝试获取锁,都是AQS帮我们处理了,我们不需要关心。
下面分独占模式和共享模式,分别介绍一下AQS中的方法,从中可以知道AQS如何维护队列,以及各个线程是如何争抢资源的。

2、独占模式申请锁:acquire()

acquire()方法是在独占模式下使用的,如果线程想要访问独占资源,需要调用该方法,如果成功,则返回,如果资源正在被其他线程访问,那么该方法会阻塞线程。ReentrantLock.lock()方法便是调用该方法完成的加锁。该方法会忽略线程中断。

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

acquire()方法里面首先调用tryAcquire()方法,尝试加锁,如果加锁失败,则调用addWaiter():

    private Node addWaiter(Node mode) {
    	//使用当前线程和独占模式做为参数创建Node节点
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //使用CAS将当前节点放到队列尾
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果上一步CAS操作没有成功,则进入enq方法,使用自旋锁和CAS操作继续执行入队操作
        enq(node);
        return node;
    }

compareAndSetTail()方法里面使用Unsafe类将当前节点设置为队列尾节点,后面凡是涉及到使用Unsafe类的方法不再详细介绍代码:

    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

addWaiter()方法首先尝试将节点放入队列尾,如果不成功,则调用enq()方法:

    private Node enq(final Node node) {
    	//自旋锁,不断循环直到成功
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
            	//如果当前队列是空的,则创建一个空的Node节点作为头结点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	//继续尝试将当前节点设置为队列尾节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

调用完addWaiter()方法将节点入队之后,调用acquireQueued()方法:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋锁
            for (;;) {
            	//取当前节点的前一个节点
                final Node p = node.predecessor();
                //如果当前节点是head节点的下一个节点,那么尝试获取锁
                //如果p == head为true,说明原来的head节点代表的线程已经释放了锁,
                //不过也有可能是当前节点被中断了(Thread..interrupt())
                if (p == head && tryAcquire(arg)) {
                	//将当前节点设置为头结点
                    setHead(node);
                    //将前一个节点的next设置为null,是为了JVM能够GC掉原来的头结点
                    //在setHead()方法里面已经将node节点的prev设置为null了
                    p.next = null; 
                    failed = false;
                    return interrupted;
                }
                //下面介绍shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //如果线程被中断,则设置返回值为true
                    interrupted = true;
            }
        } finally {
            if (failed)
            	//如果上面申请锁的过程中,抛出异常,进入下面的方法
                cancelAcquire(node);
        }
    }

接着看shouldParkAfterFailedAcquire():

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    	//创建节点的时候,waitStatus默认是0
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
			//如果前驱节点状态已经设置好了,也就是说已经告知前驱节点我在等着你通知了,
			//那么当前线程接下来就可以进入waiting状态了
            return true;
        if (ws > 0) {
			//waitStatus>0表示节点代表的线程放弃了加锁操作,那么当前节点的位置就往前挪
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
			//设置前驱节点的状态为SIGNAL,表示后继节点等待前驱节点唤醒
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt():

    private final boolean parkAndCheckInterrupt() {
    	//设置当前线程状态为waiting,当前线程进入休眠状态,等待前驱节点将自己唤醒,或者线程被中断
        LockSupport.park(this);
        //检查线程是否中断
        return Thread.interrupted();
    }

下面在回过来看一下acquireQueued()方法里面最后调用的cancelAcquire()方法,如果在尝试加锁的过程中抛出异常,那么会调用该方法:

    private void cancelAcquire(Node node) {
       	//当前节点不存在,忽略
        if (node == null)
            return;
		//设置node节点的线程对象为null,表示线程与node节点解除关联
        node.thread = null;

        Node pred = node.prev;
        //找到前面节点中第一个状态正常的节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        Node predNext = pred.next;
		//将当前节点的状态设置为CANCELLED
        node.waitStatus = Node.CANCELLED;
		//如果当前节点是队列尾,那么调整队列中的节点,将当前节点从队列尾剔除
		//如果下面的方法修改不成功,当前节点在后续处理中也不会被唤醒
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
            
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                //将当前节点从队列中剔除,并将合法的节点连接起来
                //如果当前分支操作失败,//在shouldParkAfterFailedAcquire()方法里面会继续操作
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
            	//如果当前节点是队列中第一个等待锁的节点,
            	//那么需要唤醒下一个节点开始抢锁
                unparkSuccessor(node);
            }
            node.next = node; 
        }
    }

执行完acquireQueued() 方法之后,如果当前线程被中断过,那么调用selfInterrupt()再次中断,相当于之前没有响应中断,这次补上一次:

    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

到这里,在独占模式下调用acquire()申请锁访问独占资源的流程介绍完了,这里总结一下:

  1. 调用tryAcquire方法尝试申请锁,申请成功,则可以直接访问独占资源;
  2. 如果上一步申请失败,调用addWaiter()方法将当前线程作为等待队列中的节点加入等待队列;
  3. 检查队列中当前节点的前驱节点是否是头结点,如果是,则再次尝试申请锁,如果成功则将当前节点设置为头结点,接下来可以访问独占资源,如果申请失败,修改前驱节点的状态为SIGNAL,然后线程进入waiting状态,等待被前驱节点唤醒或者线程被中断,前面这一个过程会不断循环,直到锁申请成功或者抛出异常。
  4. 上一步申请锁的过程中如果抛出异常,会将当前节点从等待队列中删除。

3、独占模式释放锁:release()

上一小节介绍了独占模式下申请锁,这里介绍一下与申请对应的释放锁release():

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
        	//head节点代表的是当前线程所在的节点
            Node h = head;
            //如果waitStatus为0,表示当前节点已经是队列尾,后面没有等待线程了
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

release()首先调用tryRelease()尝试释放锁,如果释放失败,则返回false,释放成功则找到队列的头结点,将头结点后面的等待线程唤醒:

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
        	//将当前节点的状态修改为0,我认为这里置不置0,对程序运行没有影响
        	//如果能将节点状态设置为0,release()方法可以快速返回
            compareAndSetWaitStatus(node, ws, 0);
		//首先查看当前节点的下一个节点是否是合法状态,如果是,则直接唤醒
		//如果不是,则从队列尾向前找,找到一个状态合法的节点,然后将其唤醒
        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);
    }

独占模式下,释放锁的过程比较简单,这里总结一下:

  1. 调用tryRelease()方法释放锁;
  2. 如果上一步释放成功,则找到队列的头结点,然后查看下一个节点的状态是否是等待状态,如果是,则唤醒,如果不是,则从队列尾向前找,找到一个等待的线程,然后将其唤醒。

4、共享模式申请锁:acquireShared()

acquireShared()方法是在共享模式下使用的,如果线程想要访问共享资源,需要调用该方法,如果成功,则返回,如果资源正在被其他线程访问,那么该方法会阻塞线程。该方法同样的也会屏蔽线程中断。

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

从上面方法可以看到,acquireShared()首先调用tryAcquireShared()尝试加锁,如果成功则返回,如果失败则调用doAcquireShared()入等待队列:

    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) {
               	 	//如果当前节点是head节点的下一个节点,那么尝试获取锁
                	//如果p == head为true,说明原来的head节点代表的线程已经释放了锁,
                	//不过也有可能是当前节点被中断了(Thread..interrupt())
                    int r = tryAcquireShared(arg);
					//r大于等于0,说明加锁成功
                    if (r >= 0) {
                    	//将当前节点设置为头结点,并且如果还有锁,则继续唤醒后面的节点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        //检查是否线程中断,如果有,则再调用一次补上中断
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //更改前驱节点的状态,如果更改成功,当前线程进入waiting状态,等待被唤醒
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法和独占模式的acquireQueued()很类似。下面重点看一下setHeadAndPropagate(),与独占模式相比,两者的区别主要在这个方法上:

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        //将当前节点设置为头结点
        setHead(node);
        //propagate表示剩余可以使用的锁,因为锁有多个,可以多个线程共同使用
        //因此只要下面有一个条件满足,就调用doReleaseShared()尝试唤醒后继节点
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
    private void doReleaseShared() {
        for (;;) {
            Node h = head;//尝试唤醒头结点的后继节点
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //如果当前节点的状态为Node.SIGNAL,表示后继节点等待前驱节点唤醒
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;           
                    unparkSuccessor(h);//唤醒后继节点
                }
                //将当前节点的状态设置为Node.PROPAGATE,
                //这样可以确保以后申请锁和释放锁时,都可以唤醒节点
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

setHeadAndPropagate()主要是将当前唤醒的节点设置为头结点,然后在尝试唤醒后继节点。
理解了独占模式下申请锁的流程,共享模式就会很好理解。都是先申请锁,如果不成功,则加入队列,然后线程进入waiting状态,线程休眠,被其他线程唤醒之后,尝试再尝试申请锁,申请成功了,则将当前节点设置为头结点。区别是,独占模式加锁成功后,不会在唤醒后继节点,而共享模式会继续唤醒后继节点。

5、共享模式释放锁:releaseShared()

共享模式下释放锁需要调用releaseShared()方法:

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

doReleaseShared()在前面已经介绍过了:

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            //如果队列不为空,则进入下面的if分支
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //如果后继节点在等待前驱节点唤醒,则进入下面的分支唤醒后继节点
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;           
                    unparkSuccessor(h);
                }
                //将当前节点的状态设置为Node.PROPAGATE,
                //这样可以确保以后申请锁和释放锁时,都可以唤醒节点
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;               
            }
            if (h == head) //如果头结点没有发生变化,则退出      
                break;
        }
    }

共享模式下的申请和释放锁与独占模式不同,独占模式一次只允许一个线程申请到锁,而共享模式一次可以允许多个线程申请到锁,所以在共享模式下,当等待队列中的节点申请到锁和释放锁时都会尝试唤醒后继节点,而且这个唤醒过程还可以在等待队列上传播。比如共享模式下,现在有锁5把,线程1一次性申请了5把,线程2和线程3分别要申请2把和3把,因为资源不够,后面两个线程进入等待队列,当线程1释放了锁,会唤醒线程2,线程2唤醒后因为锁还有剩余线程3也会被唤醒。
上面介绍的这些申请锁的方法都是屏蔽线程中断的,也就是在申请锁的过程中不会响应线程中断,下面介绍两个可以响应线程中断的方法。

6、acquireInterruptibly()/acquireSharedInterruptibly()

acquireInterruptibly()用于独占模式,acquireSharedInterruptibly()用于共享模式,它们的逻辑与上面介绍过的代码逻辑类似,只不过是多了对线程中断的判断,下面先看一下acquireInterruptibly():

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())//判断是否已经中断,中断则抛出异常
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())//判断是否已经中断,中断则抛出异常
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

通过对比acquireInterruptibly()和acquire(),发现两个方法的逻辑区别不大,acquire()方法里面不会抛出线程中断的异常,而且在申请锁的过程中线程中断也不会影响锁的申请,acquireInterruptibly()则不同,一旦发现线程中断了,就会抛出InterruptedException()异常。acquireSharedInterruptibly()也是相同的逻辑,不再详细介绍。

参考文章:

https://www.cnblogs.com/waterystone/p/4920797.html

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值