Java并发-同步类的底层实现AQS

Java并发-AQS

java并发包里有许多的工具类,例如如有互斥锁ReentrantLock,控制多线程执行的栅栏CountDownLatch,信号量Semaphore,闭锁CyclicBarrier(使用ReentrantLock实现间接用了AQS)等等,这些类在底层实现中都直接或者间接的使用到了AQS,本篇就从源码角度介绍AQS的作用和实现。

AQS简介

AQS是一个管理线程的底层类,它提供一个框架,用于实现阻塞锁和相关同步器(信号量、事件等)。它被设计为一个依赖于一个代表状态的原子变量(state),且对大多数类型的同步器的有用基础类。

AQS继承于AbstractOwnableSynchronizer,这个类提供方法来指定独占这个同步对象的线程对象

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }

AQS的基础使用

AQS抽象类提供了一个代表状态的原子变量state,并且提供3个方法来修改和获取这个变量

    private volatile int state;
    //获取state
    protected final int getState() {
        return state;
    }
    //修改state,通过volatile保证可见性,但是不保证原子性
    protected final void setState(int newState) {
        state = newState;
    }
    //修改state,通过UNSAFE类提供的CAS操作保证原子性
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

我们可以空过上述方法来读取或修改state,从而实现相关功能(如lock,unlock)

同时AQS提供了5个可以重写的方法来实现获取互斥锁,释放互斥锁,获取共享锁,释放共享锁,以及判断当前线程是否持有锁

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

对于要通过AQS实现的同步工具,我们只需要根据需求重写这几个方法中的一部分即可。

简单举个列子拿ReentrantLock的AQS实现来说,它是一个互斥锁实现,部分源码如下

    abstract static class Sync extends AbstractQueuedSynchronizer {
    abstract void lock();
    //非公平获取锁
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();//获取当前state值
        //如果state为0说明没有线程持有锁
        if (c == 0) {
            //cas更新state,成功则记录获取锁的线程返回成功
            //注意这边必须用cas因为在获取state到这代码中不能保证没有其他线程抢先获取了锁
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //如果当前线程已经持有了这个锁,作为可重入锁直接更新state值,代表重入一次
        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;
        //unlock后将所有重入锁释放,则锁被释放,当前持有锁线程为null
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
    //判断是否为当前线程持有锁
    protected final boolean isHeldExclusively() {
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

ReentrantLock的底层实现就是这个继承了AQS的Sync类,对外提供的api就是调用这个类中的方法,这个Sync的采用了模板设计模式,其两个子类分别实现了公平锁和非公平锁。

AQS源码详解

AQS的实现主要核心有
- state以及相关方法,用于提供给实现AQS者,从而通过改变state来实现同步
- tryAcquire,tryRelease等方法,提供给AQS实现者通过重写来实现同步获取和释放
- acquire,release等方法,调用了对应tryXxxx方法,来具体实现对线程的操作
- Node节点用来封装线程,AQS通过由Node节点组成的双链表实现的队列来实现对线程的调度,从而实现同步有关需求

其中state和tryXxxx相关上面已有提及,下面重点说明后两个有关内容。

Node节点

AQS通过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;

        static final int CONDITION = -2;

        static final int PROPAGATE = -3;

        //节点等待状态,-3,-2,-1,0,1,具体含义见下表
        volatile int waitStatus;

        //前驱节点
        volatile Node prev;

        //后继节点
        volatile Node next;

        //封装的线程
        volatile Thread thread;

        //指明节点类型,共享还是独占
        Node nextWaiter;        

        //是否为共享节点
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        //获取前驱节点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
    }
节点等待状态含义
属性名称 描述
int waitStatus 表示节点状态包括:
1 CANCELLED,值为1,表示当前的线程被取消
-1 SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行也就是unpark,也就是说后继节点是阻塞的
-2 CONDITION,值为-2,表示当前结点在等待condition,也就是在condition队列中
-3 PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
0 值为0,表示当前结点在sync队列中,等待获取锁

AQS使用Node节点构成的双链表实现了队列,并用这个队列来管理线程

private transient volatile Node head;
private transient volatile Node tail;

对于这个队列来说持有锁的线程一定是队头节点封装的线程。

这里找一张网上的图来展示下数据结构
Java并发-同步类的底层实现AQS_第1张图片

主要实现方法

AQS的具体实现依靠acquire,release,acquireShared,releaseShared来实现,它们分别会调用对应的tryXxxx方法,这是一个典型模板设计模式。

独占同步的实现-release和acquire

首先看一下acquire的源码

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

这个方法很简单,如果tryAcquire成功则说明线程成功获取锁,否则将会调用addWaiter生成Node节点,并将节点入队,然后进入无线循环直到这个节点被取消或者线程成功获取锁,在无线循环中会在达到条件时使用LockSupport.park方法来挂起线程,而不是让其一直不停的运行循环代码占用cpu资源。

tryAcquire是自己子类需要重写的一个方法代表尝试获取锁操作,并通过返回结果表示是否获取成功。下面重点来看addWaiter和acquireQueued方法

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);

        Node pred = tail;
        //存在队尾节点尝试CAS更新对尾节点指针
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

这个方法主要是创建这个线程的Node节点,如果队尾存在尝试使用CAS将队尾指针替换为指向当前节点,成功则将原来队尾的next指向当前节点返回,否则调用enq完成入队。

enq是使用无线循环CAS加失败重试的典型无锁算法方式来完成节点入队的

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //空队时cas更新队头指针成功后队尾也指向这个节点
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

acquireQueued方法则是无线循环直到该节点的线程获取锁或者被取消,它是实现线程获取锁失败后调度的主要方法

    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;
                }
                //如果前驱不是头结点或者尝试获取锁失败(被其他线程抢先获取了)
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //执行中抛出异常则取消这个节点
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法的主要思路是
- 判断前驱节点是否为头结点,如果前驱节点是头结点(头结点是持有锁的节点)那么尝试获取锁,如果成功则自己成为头节点,并且返回在等待过程中线程是否被中断(中断标示位是否被修改过)。
- 如果前驱不是头结点,或者tryAcquire失败(例如被一个不在队列中的新的线程抢先获取)则检查自己是否可以挂起,shouldParkAfterFailedAcquire
- 如果可以挂起则挂起并检查线程中断标记,parkAndCheckInterrupt
- 执行过程中抛出异常则取消节点

下面来看shouldParkAfterFailedAcquire方法

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

这个方法的思想是
- 如果前驱节点等待状态为-1,则可以挂起,因为-1代表后面节点等待unpark
- 否则如果前驱节点为1,说明前驱节点被取消,则一直往前寻找到第一个不是1的节点作为自己新的前驱节点
- 否则将前驱节点状态cas更新为-1,这样acquireQueued下一次循环调用本方法时就会返回true了

由于acquireQueued中只要线程没有获取锁或者抛出异常就会无限循环,每次都会调用shouldParkAfterFailedAcquire所以本方法中cas失败了(即没有更新为-1)也无所谓不需要在本次调用中重试

parkAndCheckInterrupt方法思路很简单,使用LockSupport.park挂起线程,并使用Thread.interrupted()判断线程中断标记位,并重置标记位

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

最后说一下等待过程中出现异常后调用的取消方法

    private void cancelAcquire(Node node) {
        if (node == null)
            return;

        node.thread = null;

        // 跳过前面所有的取消状态的节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        //设置本节点状态为取消
        node.waitStatus = Node.CANCELLED;

        // 尝试用cas移除自己,尾节点和普通节点移除方式不同
        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;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

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

取消方法思路是将自己设为取消状态,跳过前面所有的取消节点,并用cas尝试将自己从队列中移除,这里cas失败不用重试,因为之前提到的shouldParkAfterFailedAcquire方法也会跳过(移除)节点前面的取消节点的。

acquire还有一些其他的实现例如acquireInterruptibly,他们之间大同小异,只是增加了可中断或者超时等功能,例如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);
        }
    }

很明显这个方法只是在判断线程中断后之间抛出中断异常而不是在最后抛出线程在等待锁过程中是否中断的flag

带有超时功能的tryAcquireNanos

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, 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);
        }
    }

同样的它只是用带超时时间的parkNanos代替park来挂起线程,并且在每次循环判断是否超时,如果超时返回获取锁失败

说完了acquire再说一说release,release方法就是用来释放线程对锁的占有的其源码如下

    public final boolean release(int arg) {
        //tryRelease成功
        if (tryRelease(arg)) {
            Node h = head;
            //头结点必须存在且状态不能为0
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

由于头结点是持有锁的线程节点所以要释放头结点必须存在,而头结点如果为0说明后面的节点不需要unpark(见前面的分析),接下来看一下unparkSuccessor方法

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            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);
    }

思路很简单寻找头结点的后继节点,一般来说是它的next节点,但是next如果被取消则从tail开始向前找到最靠近head且不是取消的节点为unpark的节点,unpark后这个节点的线程就会继续进行它的acquireQueued方法的下一次循环了。

说完了独占方式,接下来说一下共享方式的源码,共享方式的源码主要为acquireShared和releaseRelease,其实共享方式和独占大同小异,最主要的不同时共享节点在首节点释放所并自己获取锁后会传播这个锁的释放给后面的节点从而使得后面的共享节点也能够获取到锁来运行,典型的应用有CountdownLatch和Semaphore。

acquireShared源码如下

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

主要就是调用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) {
                    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);
        }
    }

doAcquireShared方法其实和独占模式的acquireQueued方法大同小异,区别在于当前节点获取共享锁后传播共享锁,setHeadAndPropagate方法其源码如下

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; 
        //设置头结点
        setHead(node);
        //propagate是tryAcquireShared的返回大于0表示成功回去锁
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //如果后继节点是共享节点则传播release
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

大致流程网上找了个图
image

doReleaseShared源码如下

private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            if (h == head)                   
                break;
        }
    }

该方法无限循环直到头结点改变即有新的节点获取锁,当头结点状态为-1即下一个节点等待unpark时通过cas修改头结点状态,成功后唤醒后继节点,这回导致头结点改变从而结束本函数,否则如果状态为0则将状态置位传播状态-2,这是共享节点独有的状态

共享状态同样也有tryAcquireSharedNanos方法实现带有时限的获取锁操作,大致原理与互斥模式相同不再赘述。

总结

AQS是同步工具类的底层类,它使用模板设计模式,提供了同步工具类的一种便捷的实现方式,使得实现同步工具类时不需要关注底层线程的调度而专心于逻辑。工具类通过一个内部类继承这个类重写其tryXxx方法来实现所需功能,一般形式为private static class Sync extends AbstractQueuedSynchronizer

你可能感兴趣的:(java,源码)