AQS-核心概念

理解 AQS 的核心问题

AQS 是一个用于构建锁、同步器等线程协作工具类的框架,有了 AQS 以后,很多用于线程协作的工具类就都可以很方便的被写出来,可以极大地减少更上层的开发工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题。基于 AQS 的实现,JDK 已经为我们提供了多个实现线程协作工具类,此外我们可以在理解 AQS 的基础上实现自己业务的线程协作器。

带着问题理解 AQS:

  • 线程如何获取锁,都做什么处理;
  • 线程获取不到锁,应该做什么处理;
  • 获取到锁的线程释放锁后,其他线程做什么处理;

理解这几个问题,对 AQS 的理解更加立体。

AQS 的核心概念

state/等待队列/条件队列

AQS 中主要维护了 state(锁状态的表示)、等待队列(CHL)、条件队列。

  • state 是临界资源,也是锁的描述。表示有多少线程获取了锁;
  • 等待队列维护了获取不到锁的线程(Node),队列中的元素 Node 代表线程的信息;
  • 条件队列维护了获取等待条件唤醒的线程(Node);

其中 state 维护了线程获取锁的次数,当线程获取锁一次 state 加一,同一个线程获取 N 次,那么 state = N。

/**
     * The synchronization state.
     */
    private volatile int state;

    /**
     * Returns the current value of synchronization state.
     * This operation has memory semantics of a {@code volatile} read.
     * @return current state value
     */
    protected final int getState() {
        return state;
    }

    /**
     * Sets the value of synchronization state.
     * This operation has memory semantics of a {@code volatile} write.
     * @param newState the new state value
     */
    protected final void setState(int newState) {
        state = newState;
    }

等待队列(也叫 CHL 队列,同步队列)使用了双向链表实现,将获取不到锁的线程抽象为一个节点(Node 内部类),维护到链表中,使用 head 节点、tail 节点、next 指向、prev 指向进行维护。当有线程获取锁失败,就被添加到队列末尾。

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

如果锁增加条件等待和唤醒,那么 AQS 同时维护了条件队列,该队列的实现和等待队列相同,也是使用相同Node内部类,维护到链表中。如果没有对该锁调用启用条件等待的 API 时,该队列不存在。

 Condition condition = lock.newCondition();
 condition.await();

线程节点和队列

线程节点中主要维护了 模式、waitStatus、thread 和 链表信息:

  • SHARED、EXCLUSIVE:表示节点时的模式(共享、独占);
  • waitStatus:等待状态,CANCELLED 、SIGNAL、CONDITION、PROPAGATE、默认是 0;
  • thread:线程,对于等待的线程,同步器将获取尝试获取锁不成功的线程和节点关联,可以理解节点 = 线程;
  • 链表信息:prev、next 链表的指向指针,用于链表查找、插入和删除操作。
class Node {
    /**
     * 用于表示节点是共享模式下的等待节点
     */
    static final Node SHARED = new Node();
    /**
     * 用于表示节点是独占模式下的等待节点
     */
    static final Node EXCLUSIVE = null;

    /**
     * 线程被取消了
     */
    static final int CANCELLED = 1;
    /**
     * 释放资源后需唤醒后继节点
     */
    static final int SIGNAL = -1;
    /**
     * 等待condition唤醒
     */
    static final int CONDITION = -2;

    /**
     * 工作于共享锁状态,需要向后传播,比如根据资源是否剩余,唤醒后继节点
     */
    static final int PROPAGATE = -3;

    /**
     * waitStatus 用于标识线程的状态,取值有:SIGNAL、CANCELLED、CONDITION、PROPAGATE
     * 这些值按数字排列以简化使用,>=0 标识线程不需要被通知唤醒,大多数时候程序不需要
     * 检验程序的确定值,只需要看是不是 >=0 或者 <0; 就可以知道程序是否被唤醒,对于普
     * 通同步节点,该字段被初始化为 0,并且对于条件节点,该字段被初始化为 CONDITION,
     * 使用CAS进行修改。
     */
    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;
    }

    /**
     * 空构造
     */
    Node() {    // Used to establish initial head or SHARED marker
    }

    /**
     * 构造不同模式的节点
     */
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    /**
     * 构造不同状态的节点
     */
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

AQS 将获取锁的线程抽象为 Node, 获取不到锁的线程 AQS 会生成一个节点,将其插入到队列的尾部,待持锁的线程释放锁之后,再重新唤起队列中的线程。因此复杂的线程同步器转化为队列的操作。当然 AQS 显然不这么简单,但这不失为对 AQS 初步的一个理解和印象。

addWaiter 函数是源码中重要的函数,从代码来看,使用当前线程(尝试获取锁的线程)和模式(独占/共享)两个参数实例化出一个节点,判断链表(队列)是否为空,不空就将新节点插入到链表的尾部,如果为空就申请队列。抛开线程的相关知识,这是链表的最为实际的例子。理解链表的操作,便可以理解 AQS 实现大部分逻辑。

private Node addWaiter(Node mode) {
    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;
}

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;
            }
        }
    }
}

从整体上来讲,AQS 底层是使用双向链表实现的队列,将线程和和线程状态封装到一个节点中,在线程获取不到锁资源时候将线程加入到队列中,调用特定 API (LockSupport.park)将线程阻塞住,等待获取到锁资源的线程释放锁资源之后,让队列中的线程节点出队并且调用特定 API (LockSupport.unpark)唤醒,从而获取锁资源。

posted @ 2021-10-22 18:57  yaomianwei  阅读(49)  评论(0编辑  收藏  举报