为了账号安全,请及时绑定邮箱和手机立即绑定

Java 同步框架 AQS 深入分析

标签:
Java

概述

在多线程程序中,往往存在对共享资源的竞争访问,为了对共享资源进行保护,需要使用一些同步工具,比如 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 框架提供了先进先出的阻塞锁实现,在此基础上,提供了独占和共享等多种模式供使用方实现。除此之外,还提供了一个条件变量的实现。

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



作者:albon
链接:https://www.jianshu.com/p/2df967e50873


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消