Java并发学习(三)-AbstractQueuedSynchronizer

来源:互联网 发布:淘宝客服不理人 编辑:程序博客网 时间:2024/06/06 20:34

花了一段时间学习了AbstractQueuedSynchronizer,研究了源码以及博客,这篇文章主要以自己的理解去一起学习AQS。

AQS简介

AQS是简写,全称是AbstractQueuedSynchronizer,在java.util.concurrent.locks 包下面,这个类是Java并发中的一个核心类,从名字里面可以看出来,里面维护这一个双向队列(queue)。和synchronize不同,它在Java语言层面实现了锁,而synchronize则是借用了JVM以及操作系统的。所以会说Reentrant更细腻。

与ReentrantLock的关系

前面说过,AQS里面维护着一个队列,那么队列里面又是什么呢?
主要是线程,以及其他属性。
相信很多人都用过Java里面的锁,除了可以用Synchronize,同样可以用ReentrantLock来实现,ReentrantLock就是用AQS来实现的,它继承自AQS,AQS更像一个框架,如果你想实现锁,共享锁或者排他锁,你需要遵循他的规范去继承它,并且重写方法就可以实现,这里可以看我分析ReentrantLock这篇文章。

AQS队列结构

前面说过了,AQS里面维护这一个queue,它是双向的,按照类里面的含义,这条队列是先入先出的,即FIFO。那么里面的Node节点里面又是咋样的呢?

    static final class Node {        static final Node SHARED = new Node();       /** 默认是共享模式 */        static final Node EXCLUSIVE = null;          /** 排他模式 */        static final int CANCELLED =  1;             /** 表示当前线程被取消 */        static final int SIGNAL    = -1;      //表示当前节点要被进入sync时,的后继节点要被唤醒,也就是unparking         static final int CONDITION = -2;   // 表示当前节点在等待condition,也就是在condition队列中。         static final int PROPAGATE = -3;       // 表示当前节点的后续的,acquireShared 能够被执行。         volatile int waitStatus;          //初始为0 当为0的时候,就处于sync队列中,等待着获取锁。        volatile Node prev;               //在检查waitStatus情况下,的前一个节点。        volatile Node next;              //后一个节点,当被出队时就会被gc。        volatile Thread thread;         //当前执行的线程。        Node nextWaiter;          //在condition队列的下一个等待着,或者是share模式的下一个。    }

如上所示,Node节点里面有5个状态:

  • CANCELLED
  • SIGNAL
  • CONDITION
  • PROPAGATE
  • 0

具体的解释在上面代码中已经有体现。
那么这个Node,什么时候会被使用呢?
在用到锁的时候,我们有个基础的理解,如果一个线程获取不到锁,它会怎么办,它会等待继续尝试获取,或者进入挂起进入等待队列(不消耗cpu)。所以应该知道了,这里的Node是用来存储线程的。下面我将通过AQS源码从以下几个方面分析AQS的结构

AQS结构分析

先看AQS的声明:

public abstract class AbstractQueuedSynchronizer    extends AbstractOwnableSynchronizer    implements java.io.Serializable {    ...}

继承自一个父类AbstractOwnableSynchronizer 而这个父类里面,只是很简单的定义了一个当前拥有排他锁的线程:

public abstract class AbstractOwnableSynchronizer    implements java.io.Serializable {    protected AbstractOwnableSynchronizer() { }    private transient Thread exclusiveOwnerThread;    protected final void setExclusiveOwnerThread(Thread thread) {        exclusiveOwnerThread = thread;    }    protected final Thread getExclusiveOwnerThread() {        return exclusiveOwnerThread;    }}

AQS,负责管理线程状态, 子类主要通过对volatile变量state的操作去决定是否获取锁。然后再决定是否进入队列。

AQS里面的state

AQS里面有个state,它是volatile类型的,它是用来干嘛的呢?
关于volatile,可以看我这篇文章:Java并发学习(二)-JMM
首先思考下,为什么能够判定某一个线程能够获取锁呢?
答案就是这个state,在AQS里面有一些方法,专门用于去给子类重写的:

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

这里只是给了一个头,具体有子类去实现,接下来看看ReentrantLock的实现:

 protected final boolean tryAcquire(int acquires) {            return nonfairTryAcquire(acquires);        }        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;        }

上面只贴出了ReentrantLock里面的非公平锁的重写,首先调用父类的getState()获取state变量,在尝试用原子性CAS方式compareAndSetState尝试设置state的值,实际上也就是尝试获取锁。设置成功就返回true,失败的话再检查是否为同一个线程 如果同一个线程,则在一定范围下也获取成功。否则获取不成功。
关于非公平锁,可以看我的专门分析Reentrant的文章。
经过了上面的分析,大概可以清楚,AQS是如何控制资源,并且给管理线程获取状态的。

  • volatile类型的state
  • CAS原子性修改

AQS中锁的获取与释放

上一个小结中,主要通过Reentrant为例大致说明了state的作用,接下来看AQS里面获取锁的思想。
AQS给子类提供了两种类型的锁:

  • 排他锁 (EXCLUSIVE)
  • 共享锁 (SHARED)

排他锁

普通获取排他锁的过程:acquire

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

arg为用户自定义子类传回的state变量,由短路与,可以知道,当(tryAcquire)获取失败时候,才会执行&&后面条件判断。
接下来看addWaiter 方法。

    private Node addWaiter(Node mode) {        //由mode,新建node,排他        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;    }    /**     * 节点入队操作。     * 插入到队尾     * 返回插入时候的这个tail尾节点。     */    private Node enq(final Node node) {        for (;;) {            Node t = tail;            if (t == null) { //如果尾节点为null,就把头结点设为尾节点。                if (compareAndSetHead(new Node()))                    tail = head;            } else {                node.prev = t;                //否则就就把node插入到队列最后。                if (compareAndSetTail(t, node)) {                    t.next = node;                    return t;                }            }        }    }

由上代码,当获取锁失败,就需要在AQS里面记录嘛,就是插入这条队列,首先通过addWaiter 尝试插入一次,如果失败,则需要以自旋方式进行插入,一定得插入。都是利用CAS方式替换tail。

可中断获取排他锁:doAcquireInterruptibly

    /**     * 在排他锁模式下,可中断的获取锁。     * Acquires in exclusive interruptible mode.     * @param arg the acquire argument     */    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)            //取消获取,状态设为cancelled。                cancelAcquire(node);        }    }

同样是先通过自旋的方式获取锁,如果自旋多次,仍然没有获取,并且前置节点已经发生了变化,则需要的时候将线程阻塞。具体什么变化呢?
接下来看shouldParkAfterFailedAcquire

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {        int ws = pred.waitStatus;        if (ws == Node.SIGNAL)            /*这个节点已经成功设置了状态,请求获取锁,所以只需要等            待前屈节点运行玩就会unpark自己,所以可以进行park。             * This node has already set status asking a release             * to signal it, so it can safely park.             */            return true;        if (ws > 0) {            /*             * 跳过被cancell的。             */            do {                node.prev = pred = pred.prev;            } while (pred.waitStatus > 0);            pred.next = node;        } else {            /*             * condition或者 在sync,propagate             * 尝试设为signal。             */            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);        }        return false;    }

然后进行park操作:

 private final boolean parkAndCheckInterrupt() {     //调用native方法进行park        LockSupport.park(this);        return Thread.interrupted();    }

如果获取失败,则需要cancell,看cancelAcquire ,代码比较复杂,看慢慢分析:

    /**     * 当获取不到就会取消     *一旦发生异常,导致获取锁失败,则会调用cancelAcquire()方法"Cancels an ongoing attempt to acquire"。     *  node是tail     *  node是head     *  node既不是tail,又不是head     * @param node the node     */    private void cancelAcquire(Node node) {        // Ignore if node doesn't exist        if (node == null)            return;        //1. node不再关联到任何线程        node.thread = null;        //2. 跳过被cancel的前继node,找到一个有效的前继节点pred        // Skip cancelled predecessors        Node pred = node.prev;        while (pred.waitStatus > 0)            node.prev = pred = pred.prev;        // predNext is the apparent node to unsplice. CASes below will        // fail if not, in which case, we lost race vs another cancel        // or signal, so no further action is necessary.        Node predNext = pred.next;        //3. 将node的waitStatus置为CANCELLED        // Can use unconditional write instead of CAS here.        // After this atomic step, other Nodes can skip past us.        // Before, we are free of interference from other threads.        node.waitStatus = Node.CANCELLED;        //4. 如果node是tail,更新tail为pred,并使pred.next指向null        // If we are the tail, remove ourselves.        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;            //5. 如果node既不是tail,又不是head的后继节点            //则将node的前继节点的waitStatus置为SIGNAL            //并使node的前继节点指向node的后继节点(相当于将node从队列中删掉了)            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 {            //6. 如果node是head的后继节点,则直接唤醒node的后继节点                unparkSuccessor(node);            }            node.next = node; // help GC        }    }

上述cancelAcquire 一共有6步

  1. node不再关联到任何线程
  2. 跳过被cancel的前继node,找到一个有效的前继节点pred
  3. 将node的waitStatus置为CANCELLED
  4. 如果node是tail,更新tail为pred,并使pred.next指向null
  5. 如果node既不是tail,又不是head的后继节点则将node的前继节点的waitStatus置为SIGNAL并使node的前继节点指向node的后继节点(相当于将node从队列中删掉了)
  6. 如果node是head的后继节点,则直接唤醒node的后继节点

超时获取排它锁doAcquireNanos:

超时获取排它锁中,主要就是添加了nanosTimeout 这个参数,用来判定是否已经过了设定时间,从而执行不同逻辑,其他的都与可中断排它锁一致,就不多说了。

    private boolean doAcquireNanos(int arg, long nanosTimeout)            throws InterruptedException {        if (nanosTimeout <= 0L)            return false;        final long deadline = System.nanoTime() + nanosTimeout;        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 true;                }                nanosTimeout = deadline - System.nanoTime();                if (nanosTimeout <= 0L)                    return false;                if (shouldParkAfterFailedAcquire(p, node) &&                    nanosTimeout > spinForTimeoutThreshold)                    LockSupport.parkNanos(this, nanosTimeout);                if (Thread.interrupted())                    throw new InterruptedException();            }        } finally {            if (failed)                cancelAcquire(node);        }    }

释放排它锁release

有获取锁当然也会有释放所,在ReentrantLock里面使用unlock来实现,这里主要讲当ReentrantLock调用到父类AQS代码时候操作:

    /**     * 释放排他锁。     * 首先tryRelese释放标记     * 然后,排它锁获取的肯定是head出现,此时我只要唤醒(unpack)继任线程就可以了。     */    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是由子类重写的方法,目的是为了改变state的值,从而代表释放资源。当释放成功后,unparkSuccessor(h),释放头结点的后继节点。

    /**     * 如果继任者存在,唤醒继任者开始执行。     * 如果继任者waitStatus<0,则会将其设为=0进行     * 如果继任者>0(状态无效为cancelled。),则接着往后面找     */    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;        //如果ws<0,则需要设为0,因为:值为0,表示当前节点在sync队列中,等待着获取锁。        if (ws < 0)            compareAndSetWaitStatus(node, ws, 0);        Node s = node.next;        //重新找一个successor.        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);    }

unparkSuccessor方法的作用就是唤醒继任节点,因为一般当前运行的节点会设为head,所以当要释放的时候,unpark后面这个节点就可以了。
当然需要过滤掉cancelled的节点。

共享锁

共享锁,就是指可以允许多个线程同时对某一资源进行访问,这里会有个最开始的问题,会不会有并发问题?因为AQS只是提供了一个并发的核心框架,具体是否有并发问题,要看子类的自我实现,以及子类工具的用途。
什么意思呢?
就好比说,exclusive模式下,AQS管理一次只能一个线程进行运行,运行完后按照规则传递给后继节点。
而share模式下,AQS管理的则是一次能让多个线程同时运行,可以设置同时进入的数量,运行完后再把排它锁状态往后延伸。

共享锁的普通获取acquireShared

首先看acquireShared方法:

    /**     * 获取共享锁。     */    public final void acquireShared(int arg) {        if (tryAcquireShared(arg) < 0)            doAcquireShared(arg);    }    /**     * 获取共享锁。     * Acquires in shared uninterruptible mode.     * @param arg the acquire argument     */    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) {                    //首先是这样去尝试获取共享锁。通过volatile变量。                    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);        }    }

和获取排他锁思路基本一致,都是先尝试获取,获取不到在进行park操作,但是还有两个地方不同:

  • int r = tryAcquireShared(arg); if (r >= 0)
  • setHeadAndPropagate(node, r);

第一个是通过子类实现的tryAcquireShared方法去判定是否能获取锁,还记得,最开始acquireShared里面也是执行了这个方法,所以这里是被执行了2次的,最后判断是否能够获取锁是通过r>=0来判断的,所以是个范围,也就印证了这是个共享的锁。

第二个的setHeadAndPropagate,和排它锁(only setHead)不同,主要是为了设置head,以及把共享状态往后穿,大家应该还记得Node节点的waitState属性里面有个Propagate的值-3吧。

    /**     * 设置头结点。     * tryAcquireShared执行相关。     * @param node the node     * @param propagate the return value from a tryAcquireShared     * 某个节点被设置为head之后,如果它的后继节点是SHARED状态的,那么将继续通过     * doReleaseShared方法尝试往后唤醒节点,实现了共享状态的向后传播。     */    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();        }    }

doAcquireSharedInterruptibly以及doAcquireSharedNanos

超时获取以及可中断获取,在获取锁代码逻辑上与acquireShared基本一致,而在interruption以及超时判断上,则与排它锁一致,这里就不重复来说了,可以参看上面文章。

ConditionObject

还记得上面说过,在AQS的Node节点里面,有个waitState值为Condition值为-2.就是代表他在阻塞状态,类似于synchronize里面的wait,而ConditionObject也就可以充当Lock的wait和signal。后续会介绍Condition类具体分析。

先看ConditionObject里面内容:

 public class ConditionObject implements Condition, java.io.Serializable {        ...        /** 引用的头节点。 */        private transient Node firstWaiter;        /** 引用的尾节点。. */        private transient Node lastWaiter;        ...}

如上,在ConditionObject里面包含了两个重要的字段,都是Node类型,firstWaiter和lastWater;其实ConditionObject和AQS里面那条队列并不是同一条,由代码可以分析出来,请看我下文分析。

当然,里面方法主要就是await和signal。

  • private Node addConditionWaiter(),添加当前线程作为waiter
  • private void doSignal(Node first),唤醒firstWaiter
  • private void doSignalAll(Node first),唤醒,所有waiter
  • private void unlinkCancelledWaiters(),删除cancell的节点
  • public final void signal(),对外唤醒任意一个,主要第一个
  • public final void signalAll(),对外唤醒所有
  • public final void awaitUninterruptibly(),不会中断的等待
  • private int checkInterruptWhileWaiting(Node node),判断等待过程中是否发生了异常
  • private void reportInterruptAfterWait(int interruptMode),等待后重新报告异常
  • public final void await() throws InterruptedException ,对外的等待方法
  • final boolean isOwnedBy(AbstractQueuedSynchronizer sync) ,判断是否为当前线程condition
  • protected final boolean hasWaiters(),是否有waiter。

ConditionObject的主要就是用来阻塞队列以及唤醒队列的,而阻塞队列,一方面是把waitState改为condition,另一方面还需要执行park的native方法,而对于唤醒队列,则是把waitState改为0,执行unpark方法。思路理清楚了,接下来分析两个具体代码:

await方法

      public final void await() throws InterruptedException {            if (Thread.interrupted())                throw new InterruptedException();            Node node = addConditionWaiter();            //首先释放锁            int savedState = fullyRelease(node);            int interruptMode = 0;            while (!isOnSyncQueue(node)) {                //如果不在syncqueue里面,即waitState != 0,阻塞本线程                LockSupport.park(this);                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)                    //中断了就直接退出                    break;            }            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)                interruptMode = REINTERRUPT;            if (node.nextWaiter != null) // clean up if cancelled                //清楚cancelled的节点                unlinkCancelledWaiters();            if (interruptMode != 0)            //判断,是纪录异常还是抛出                reportInterruptAfterWait(interruptMode);        }

如上代码,
1. 首先会addConditionWaiter方法,主要作用就是把本线程添加到condition队列的尾端,注意是根据lastWaiter来判定的,所以添加到的是condition队列,waitState为condition。
2. 其次,fullyRelease来释放当前线程锁,也就是占有的资源。
3. 下一步,利用自旋的方式isOnSyncQueue,判断是否仍然处于等待获取资源状态。如果是的就阻塞本线程,有错误就退出。
4. 最后判断是否有错误,以及是否被设置为抛出错误,ConditionObject里面用THROW_IEREINTERRUPT 来判断。

        /**         * 重新报告interrupt类型。         */        private void reportInterruptAfterWait(int interruptMode)            throws InterruptedException {            if (interruptMode == THROW_IE)                throw new InterruptedException();            else if (interruptMode == REINTERRUPT)                selfInterrupt();        }

signal方法

ConditionObject里面有两个signal,一个是有FIFO,唤醒头结点,另一个是signalAll,唤醒所有节点。
接下来看signal方法:

        /**         * 默认唤醒第一个节点线程。         */        public final void signal() {            if (!isHeldExclusively())                throw new IllegalMonitorStateException();            Node first = firstWaiter;            if (first != null)                doSignal(first);        }

如上里面会调用doSignal(first):

        /**         * 唤醒一个waiter。         * 放入sync队列。         * @param first (non-null) the first node on condition queue         */        private void doSignal(Node first) {            do {                if ( (firstWaiter = first.nextWaiter) == null)                    lastWaiter = null;                first.nextWaiter = null;            } while (!transferForSignal(first) &&                     (first = firstWaiter) != null);        }

自旋阻塞式的,直到成功被唤醒,具体细节在transferForSignal(first)方法里面:

    /**     * 唤醒节点,状态改为0。     * 唤醒成功的话,就插入队尾并且如果此时队尾元素cancell或者强行设置队尾元素失败,那么就需要唤醒此时队尾元素。     */    final boolean transferForSignal(Node node) {        /*         * If cannot change waitStatus, the node has been cancelled.         * 设置成功了!         */        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))            return false;        /*         * 把前一个节点设为signnal。         * Splice onto queue and try to set waitStatus of predecessor to         * indicate that thread is (probably) waiting. If cancelled or         * attempt to set waitStatus fails, wake up to resync (in which         * case the waitStatus can be transiently and harmlessly wrong).         */         //放到最后重新排队。        Node p = enq(node);        int ws = p.waitStatus;        //检查前一个节点是否为signal。        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))            LockSupport.unpark(node.thread);        return true;    }

transferForSignal方法的目的就是讲节点waitState值设为0,并且添加到AQS队列的尾端。

收获

看了多遍源码即博客,才慢慢理解了AQS运行的原理,Doug Lea大佬真是厉害,从AQS里面也读到了两个比较有特色的写法:

  1. 使用&& 和||短路与和短路或去实现if的操作,里面用的真多。
  2. 在一个条件判断中,我发现同样的条件用&&连接了两次,后来知道是为了检查两次二故意做的。因为在并发环境下,很可能多做一次,就得到想要的结果了。
    /**     * 获取队列第一条线程。     * 有头结点就判断头结点,没有头结点,就从尾端一直找到头结点。     * Version of getFirstQueuedThread called when fastpath fails     */    private Thread fullGetFirstQueuedThread() {        Node h, s;        Thread st;        //两段判断一模一样,检查了两次        if (((h = head) != null && (s = h.next) != null &&             s.prev == head && (st = s.thread) != null) ||            ((h = head) != null && (s = h.next) != null &&             s.prev == head && (st = s.thread) != null))            return st;        //审查不通过,那就从尾端开始        Node t = tail;        Thread firstThread = null;        while (t != null && t != head) {            Thread tt = t.thread;            if (tt != null)                firstThread = tt;            t = t.prev;        }        return firstThread;    }

分析有诸多不足之处,如有错误,还请指出~

参考文章:
http://ifeve.com/introduce-abstractqueuedsynchronizer/
https://www.cnblogs.com/leesf456/p/5350186.html

阅读全文
0 0
原创粉丝点击