美文网首页程序员java进阶干货
Java 同步框架 AQS 深入分析

Java 同步框架 AQS 深入分析

作者: albon | 来源:发表于2017-09-13 22:51 被阅读194次

    [TOC]

    概述

    在多线程程序中,往往存在对共享资源的竞争访问,为了对共享资源进行保护,需要使用一些同步工具,比如 synchronized、ReentrantLock、Semaphore、ReentrantReadWriteLock等,后三种都是在同步框架 AQS(即 AbstractQueuedSynchronizer)的基础上构建的。

    下面这段话来自 AQS 的代码文档,描述了其设计意图:提供一个框架用于实现依赖先进先出 FIFO 等待队列的阻塞锁和相关同步器。

    Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.

    下面我们重点分析一下同步框架 AQS 的实现原理和用法。

    实现

    一个同步器有两个基本功能:获取同步器、释放同步器。AQS 提供了多种锁模式:

    1. 独占、共享
    2. 可中断、不可中断
    3. 带超时时间的

    设计模式

    AQS 采用了标准的模版方法设计模式,对外提供的是以下的方法:

        // 独占模式
        public final void acquire(int arg);
        public final boolean release(int arg);
        // 独占可中断
        public final void acquireInterruptibly(int arg)
                throws InterruptedException;
        // 独占带超时时间的
        public final boolean tryAcquireNanos(int arg, long nanosTimeout);
    
        // 共享模式
        public final void acquireShared(int arg);
        public final boolean releaseShared(int arg);
        // 共享可中断
        public final void acquireSharedInterruptibly(int arg)
                throws InterruptedException;
        // 共享带超时时间
        public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException;
    

    这些方法上都带了 final 关键字,也就是说不允许重写,那么哪些方法可以重写呢?

    //独占模式
    protected boolean tryAcquire(int arg);
    protected boolean tryRelease(int arg);
    
    //共享模式
    protected int tryAcquireShared(int arg);
    protected boolean tryReleaseShared(int arg);
    
    //是否是独占模式
    protected boolean isHeldExclusively();
    

    这些模版方法在代码中起什么作用呢?请看他们的调用方,在 acquire 方法中,当 tryAcquire 返回 true 则表示已经获得了锁,否则先 addWaiter 进入等待队列,再 acquireQueued 等候获取锁。acquireInterruptibly 也是类似的,区别只是对中断的处理不同。

        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
        public final void acquireInterruptibly(int arg)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            if (!tryAcquire(arg))
                doAcquireInterruptibly(arg);
        }
    

    以 acquire 为例,真正的排队阻塞等待锁是在 addWaiter 和 acquireQueued 中,那么 tryAcquire 方法需要做什么事儿呢?我们先看一下 ReentrantLock 中公平锁 FairSync 和非公平锁 NonfairSync 的实现:

        // 公平锁
        static final class FairSync extends Sync {
            // omit a lot
            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 {
            // omit a lot
            protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
        }
        abstract static class Sync extends AbstractQueuedSynchronizer {
            // omit a lot
            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;
            }
    }
    

    公平锁的 tryAcquire 方法所做事情大致如下:

    1. 检查了锁的状态,如果锁不被任何线程占用,且没有等待中的线程,则尝试用 CAS 设置状态,成功则 setExclusiveOwnerThread 设置占用锁的线程为当前线程。
    2. 如果当前线程已经占有了该锁,则 setState 更新更新重入次数。
    3. 上述两条都失败,则表示无法立即获得锁,返回 false。

    非公平锁的 tryAcquire 也大致一样,只是缺少判断是否有等待中的线程,而是尽最大努力去获取锁。他们的共同点是更新锁状态 state,设置当前占用锁的线程 setExclusiveOwnerThread,其中最关键的是更新锁状态 state

    值得注意的是,为何 tryAcquire 这些方法不是抽象方法,而是提供了一个默认的抛异常的方法呢?因为 AQS 中包含多种模式,而实际使用者一般只需要一种,如果不提供默认拒绝的实现,那就需要使用方去手动覆盖,反而显得啰嗦了。

        // in AQS
        protected boolean tryAcquire(int arg) {
            throw new UnsupportedOperationException();
        }
        protected int tryAcquireShared(int arg) {
            throw new UnsupportedOperationException();
        }
    

    CHL 队列

    在 AQS 中,提供了一个基于 CHL 队列的先进先出的阻塞锁。CHL 队列的入队出队操作是不需要加锁的,只是在入队成功后等待资源释放时会阻塞。在Java锁的种类以及辨析(二):自旋锁的其他种类里也提到了一种基于自旋的 CHL 锁。

    CHL 队列本质上就是一个基于 CAS 实现的双向链表,其实也算“无锁队列”了,其节点 Node 如下所示:

        static final class Node {
            volatile int waitStatus; // 节点状态
            volatile Node prev;      // 前置节点
            volatile Node next;      // 后置节点
            volatile Thread thread;  // 所属线程
            Node nextWaiter;         // 可以用于标记独占 OR 共享模式,也可以用来记录等待条件变量的下一个节点
        }
    

    当 nextWaiter 用于标记独占或共享时,其值可以为:

    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;
    

    waitStatus 的值可以有以下几种:

    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;
    /**
    * waitStatus value to indicate the next acquireShared should
    * unconditionally propagate
    */
    static final int PROPAGATE = -3;
    

    入队

    CHL 队列的入队操作间 addWaiter 方法,入队采用 CAS 自旋重试,总是添加到尾节点:

        // 尾节点
        private transient volatile Node head;
        // 头节点
        private transient volatile Node tail;
        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;
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            enq(node);
            return node;
        }
        private Node enq(final Node node) {
            for (;;) {
                Node t = 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;
                    }
                }
            }
        }
    

    出队

    如果当前节点的前任节点是头节点,则尝试获得锁,成功则出队,并且返回。下面代码的第 8-11 行为出队操作,所做事情为:把本节点 node 置为头节点,并且把新旧头节点直接的关联字段置空(新节点 prev 和旧节点 next)。

    注:旧头节点置空是为了什么呢?已经无用的 Node 能够被 GC 回收掉。

        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);
            }
        }
        private void setHead(Node node) {
            head = node;
            node.thread = null;
            node.prev = null;
        }
    

    AQS 之 ConditionObject

    在 AQS 中,还有一个 ConditionObject 类,顾名思义是一个条件变量,提供了类似于 Object wait/notify 的机制。

    ConditionObject 实现了 Condition 接口,提供了多种条件变量等待接口:可中断、不可中断、超时机制,以及两种唤醒机制:单个、全部。

    public interface Condition {  
        // 在条件变量上等待
        void await() throws InterruptedException;  
        void awaitUninterruptibly();  
        long awaitNanos(long nanosTimeout) throws InterruptedException;  
        boolean await(long time, TimeUnit unit) throws InterruptedException;  
        boolean awaitUntil(Date deadline) throws InterruptedException;  
        // 唤醒
        void signal();  
        void signalAll();  
    }  
    

    await

    await 方法类似于 Object#wait 方法,调用之前需要先获得锁。

            public final void await() throws InterruptedException {
                if (Thread.interrupted())
                    throw new InterruptedException();
                // 将当前线程添加到条件队列里
                Node node = addConditionWaiter();
                // 是否锁资源,也就是说调用 await 之前要先获得锁
                int savedState = fullyRelease(node);
                int interruptMode = 0;
                // 循环阻塞等待,直到被中断,或进入 AQS 锁的同步队列
                while (!isOnSyncQueue(node)) {
                    LockSupport.park(this);
                    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                        break;
                }
                // signal 之后,进入同步队列,再通过 acquireQueued 竞争锁
                if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                    interruptMode = REINTERRUPT;
                if (node.nextWaiter != null) // clean up if cancelled
                    unlinkCancelledWaiters();
                // 中断处理
                if (interruptMode != 0)
                    reportInterruptAfterWait(interruptMode);
            }
    

    signal

    signal 方法类似于 Object#notify 方法,主要功能是唤醒下一个等待中的线程。

            public final void signal() {
                if (!isHeldExclusively())
                    throw new IllegalMonitorStateException();
                Node first = firstWaiter;
                if (first != null)
                    doSignal(first);
            }
            // 唤醒一个节点
            private void doSignal(Node first) {
                do {
                    if ( (firstWaiter = first.nextWaiter) == null)
                        lastWaiter = null;
                    first.nextWaiter = null;
                } while (!transferForSignal(first) &&
                         (first = firstWaiter) != null);
            }
        final boolean transferForSignal(Node node) {
            if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
                return false;
            Node p = enq(node);
            int ws = p.waitStatus;
            // 唤醒该节点
            if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
                LockSupport.unpark(node.thread);
            return true;
        }
    

    用法

    ConditionObject 常用在各种队列的实现中,比如 ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue等,这里我们以 ArrayBlockingQueue 为例学习一下其用法。

    请看下面 ArrayBlockingQueue 的代码里锁和条件变量的用法,条件变量是跟锁绑定的,这里一个锁对应多个条件变量:队列非空、队列非满。

    public class ArrayBlockingQueue<E> extends AbstractQueue<E>
            implements BlockingQueue<E>, java.io.Serializable {
        /** Main lock guarding all access */
        final ReentrantLock lock;
        /** Condition for waiting takes */
        private final Condition notEmpty;
        /** Condition for waiting puts */
        private final Condition notFull;
    
        public ArrayBlockingQueue(int capacity, boolean fair) {
            if (capacity <= 0)
                throw new IllegalArgumentException();
            this.items = new Object[capacity];
            lock = new ReentrantLock(fair);
            notEmpty = lock.newCondition();
            notFull =  lock.newCondition();
        }
    }
    

    在向队列中存放元素时:

    1. 获得锁
    2. 如果队列满,则调用 notFull.await() 等待
    3. 队列不满时,则插入数据,并且向 notEmpty 条件变量发 signal 信号
    4. 解锁
        public void put(E e) throws InterruptedException {
            checkNotNull(e);
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == items.length)
                    notFull.await();
                insert(e);
            } finally {
                lock.unlock();
            }
        }
        private void insert(E x) {
            items[putIndex] = x;
            putIndex = inc(putIndex);
            ++count;
            notEmpty.signal();
        }
    

    在从队列里取元素时:

    1. 加锁
    2. 如果队列空,则调用 notEmpty.await() 等待
    3. 队列非空,则取数据,并且向 notFull 条件变量发送 signal 信号
    4. 解锁
        public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == 0)
                    notEmpty.await();
                return extract();
            } finally {
                lock.unlock();
            }
        }
        private E extract() {
            final Object[] items = this.items;
            E x = this.<E>cast(items[takeIndex]);
            items[takeIndex] = null;
            takeIndex = inc(takeIndex);
            --count;
            notFull.signal();
            return x;
        }
    

    总结

    AQS 框架提供了先进先出的阻塞锁实现,在此基础上,提供了独占和共享等多种模式供使用方实现。除此之外,还提供了一个条件变量的实现。

    锁是一种线程同步机制,用于保护对临界资源的访问。条件变量提供了一个“等待 - 唤醒”的机制,在阻塞队列里起到了生产者和消费者之间的通信的作用。

    相关文章

      网友评论

        本文标题:Java 同步框架 AQS 深入分析

        本文链接:https://www.haomeiwen.com/subject/riutsxtx.html