玩转并发-深入剖析AQS

概述

AQS(AbstractQueuedSynchronizer类)是一个用来构建锁和同步器的框架,它在内部定义了一个int state变量,用来表示同步状态.在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建。

AQS原理

AQS是通过一个双向的FIFO,依赖队列来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。该队列又被称为CLH队列。在这里插入图片描述
红色节点为头结点,可以把它当做正在持有锁的节点。

	public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    //队列节点内部类
    static final class Node {...}
	//队列头部
	private transient volatile Node head;
    //队列尾部
    private transient volatile Node tail;
    //同步状态
    private volatile int state;
}

Node

static final class Node {
      
      	//该等待的同步节点处于共享模式
        static final Node SHARED = new Node();
        //该等待的同步节点处于独占模式
        static final Node EXCLUSIVE = null;  
        //等待状态,这个和state是不同的,有1,-1,-2,-3,0五个值
        volatile int waitStatus;
        static final int CANCELLED =  1; 
        static final int SIGNAL    = -1
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
		//前驱节点
        volatile Node prev;
        //后继节点
        volatile Node next;
        //等待锁的线程
        volatile Thread thread;
        //和节点是否共享有关
        Node nextWaiter;
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

waitStatus的含义:

  • CANCELLED:该节点的线程可能由于超时或被中断而处于被取消的状态,一旦处于这个状态,节点状态将一直处于这个状态,应该从队列中移除
  • SIGNAL:表该节点的后继节点当前是阻塞的,因此当前节点在释放和取消之后,必须唤醒它的后继节点
  • CONDITION:该节点的线程处于等待条件状态,不会被当做是同步队列上的节点,直到被唤醒(signal),设置其值为0,重新进入阻塞状态.
  • PROPAGATE:共享模式下无条件所有等待线程尝试抢占锁
  • 0:新加入的节点

在锁的获取中,并不是只有一个线程才可以持有这个锁,所以此时有了独占模式和共享模式之分,也就是在Node节点中由nextWait来标识。比如ReentrantLock就是一个独占锁,只能有一个线程获得锁,而WriteAndReadLock的读锁则能由多个线程同时获取,但它的写锁则只能由一个线程持有。

  • 独占模式:只能有一个线程持有锁
  • 共享模式下:可以有多个线程持有锁

AbstractQueuedSynchronizer是个抽象类,部分方法并未实现,子类可以根据实际情况实现全部或部分方法:

//非堵塞获取独占资源,true表示成功
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    //非堵塞释放独占资源,true表示成功
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    //非堵塞获取共享资源,负数表示失败,0表示成功但不需要向后传播,大于0表示成功且可以向后传播
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //非堵塞释放共享资源,true表示成功
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //在排它模式下,状态是否被占用
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

独占模式同步状态的获取

同步状态的获取运用了模板设计模式,AQS实现acquire()的算法骨架,由子类实现tryacquire()方法

流程

  1. 调用tryAcquire,如果返回false,表示获取资源失败,要进行排队获取
  2. 调用addWaiter,创建独占模式Node,并加入到等待队列的尾部;
  3. 调用acquireQueued方法,按照线程加入队列的顺序获取资源;
  4. 如果acquireQueued返回true,表示发生中断,因此通过selfInterrupt中断当前线程(注意:acquire方法会忽略中断,当中断发生时,并不会马上退出;)
/**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

该方法首先尝试获取锁( tryAcquire(arg)的具体实现定义在了子类中),如果获取到,则执行完毕,否则通过addWaiter(Node.EXCLUSIVE), arg)方法把当前节点添加到等待队列末尾,并设置为独占模式,

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;
        //如果tail不为空,则将节点插入末尾
        if (pred != null) {
            node.prev = pred;
            //CAS操作,确保原子性
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //
        enq(node);
        return node;
    }
//前面CAS入列失败,这次自旋入列直到成功
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //如果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;
                }
            }
        }
    }

观察上面的代码,我们可以发现:

  • head节点实际上是个空节点;
  • head节点是通过new Node()创建,因此waitStatus==0;
  • 新入列的节点是通过Node(Thread thread, Node mode)创建,waitStatus==0;

在把node插入队列末尾后,它并不立即挂起该节点中线程,因为在插入它的过程中,前面的线程可能已经执行完成,所以它会先进行自旋操作acquireQueued(node, arg),尝试让该线程重新获取锁!当条件满足获取到了锁则可以从自旋过程中退出,否则继续。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋操作
            for (;;) {
            	//获取前继节点
                final Node p = node.predecessor();
                //如果前继节点为head,head为当前持有锁的节点
                //并且尝试获取锁成功,则设置传入的节点为头节点,令其持有锁,并返回
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果没获取锁,则判断是否应该挂起
                //通过该节点的前继节点的waitState确定
                //如果该节点的前继节点的waitState为SIGNAL,则挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

判断节点是否应该挂起:

//判断节点是否应该挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		//如果acquireQueued第一次调用该方法,ws==0
        int ws = pred.waitStatus;
        //waitStatus为SIGNAL表示要通过unpark唤醒后一节点,因此当acquire获取失败,需要挂起当前节点
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            
            return true;
        //如果前一节点被取消,则往前找,其实就是从队列中删除ws为CANCELLED的节点
        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.
             */
            //更新前节点的ws为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        //返回false,表示不需要park
        return false;
    }

如果前继节点的waitStatus:

  • 为SIGNAL,则返回true,表示应该挂起当前线程。
  • 大于0,将前继节点踢出队列,返回false
  • 小于0,也是返回false,不过先将前驱节点waitStatus设置为SIGNAL,使得下次判断时,将当前节点挂起.
 //park是堵塞的意思,unpark是唤醒
  private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

可以看到对于等待队列中的节点,shouldParkAfterFailedAcquire会将前节点的状态改为Node.SIGNAL;接着在下一次循环中调用parkAndCheckInterrupt堵塞线程

最后取消获取锁:

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        //如果该节点ws为CANCELLED,则继续往前找,直到找到正常的节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
            
        Node predNext = pred.next;
        //将节点状态设置为CANCELLED,即将踢出队列
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        //如果node为tail节点,则将pred更新为tail节点
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

AQS提供了acquire(int arg)方法以供独占式获取同步状态,但是该方法对中断不响应,对线程进行中断操作后,该线程会依然位于CLH同步队列中等待着获取同步状态。
为了响应中断,AQS提供了acquireInterruptibly(int arg)方法,该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断抛出异常InterruptedException

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

获取独占锁小结

AQS的模板方法acquire通过调用子类自定义实现的tryAcquire获取同步状态失败后->将线程构造成Node节点(addWaiter)->将Node节点添加到同步队列对尾(addWaiter)->节点以自旋的方法获取同步状态(acquirQueued)。在节点自旋获取同步状态时,只有其前驱节点是头节点的时候才会尝试获取同步状态,如果该节点的前驱不是头节点或者该节点的前驱节点是头节点单获取同步状态失败,则判断当前线程需要阻塞,如果需要阻塞则需要被唤醒过后才返回。
在这里插入图片描述

独占模式同步状态的释放

AQS中的release释放同步状态和acquire获取同步状态一样,都是模板方法,tryRelease释放的具体操作都有子类去实现,父类AQS只提供一个算法骨架。

 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()方法释放锁,然后唤醒后继节点,在唤醒的过程中,后继节点判断自己此时是不是头节点且判断自身状态,满足则唤醒这个后继节点,否则从tail节点向前寻找合适的节点,如果找到,则唤醒.


共享模式同步状态的获取

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

其中tryAcquireShared()方法由子类实现。方法首先是调用tryAcquireShared(int arg)方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功

//尝试获取同步状态
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //如果前继节点为head,则继续尝试获取同步状态
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //返回非负数,说明获取同步状态成功
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        //将节点设置为head,原先head的后驱节点设null,方便GC回收
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //判断节点是否应该挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

可以看到acquire调用的是setHead,而acquireShared调用的是 setHeadAndPropagate


setHeadAndPropagate()方法

setHeadAndPropagate()方法负责将自旋等待或被 LockSupport 阻塞的线程唤醒

 private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
       //if条件
       //propagate>0
       //head为null
       // 之前操作已经设置了后续节点需要唤醒的
        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;
                if (ws == Node.SIGNAL) {//如果ws为SIGNAL,则需要unpark后续节点
                    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;
        }
    }

从上面的分析可以知道,独占模式和共享模式的最大区别在于独占模式只允许一个线程持有资源,而共享模式下,当调用doAcquireShared时,会看后续的节点是否是共享模式,如果是,会通过unpark唤醒后续节点;
从前面的分析可以知道,被唤醒的节点是被堵塞在doAcquireShared的parkAndCheckInterrupt方法,因此唤醒之后,会再次调用setHeadAndPropagate,从而将等待共享锁的线程都唤醒,也就是说会将唤醒传播下去

  • 加入同步队列并阻塞的节点,它的前驱节点只会是SIGNAL,表示前驱节点释放锁时,后继节点会被唤醒。shouldParkAfterFailedAcquire()方法保证了这点,如果前驱节点不是SIGNAL,它会把它修改成SIGNAL
  • 造成前驱节点是PROPAGATE的情况是前驱节点获得锁时,会唤醒一次后继节点,但这时候后继节点还没有加入到同步队列,所以暂时把节点状态设置为PROPAGATE,当后继节点加入同步队列后,会把PROPAGATE设置为SIGNAL,这样前驱节点释放锁时会再次doReleaseShared,这时候它的状态已经是SIGNAL了,就可以唤醒后续节点了

猜你喜欢

转载自blog.csdn.net/weixin_40288381/article/details/87706779