Java并发编程之AbstractQueuedSynchronizer详解(二)

同步器依赖于内部的FIFO双向等待队列来完成同步状态的管理,该等待队列是CLH队列的变种,CLH队列通常用于自旋锁,同步器中的等待队列可以简单的理解为“等待锁的线程队列”。当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,Node类的定义如下:

static final class Node {
    /** 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 value to indicate thread has cancelled */
    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;

    volatile int waitStatus;


    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    
    Node nextWaiter;

    /**
        * Returns true if node is waiting in shared mode.
        */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
        * Returns previous node, or throws NullPointerException if null.
        * Use when predecessor cannot be null.  The null check could
        * be elided, but is present to help the VM.
        *
        * @return the predecessor of this node
        */
    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;
    }
}

节点的属性类型与名称以及描述如下表所示:

属性类型与名称

描  述

Node SHARED

表示节点使用共享模式等待

Node EXCLUSIVE

表示节点使用独占模式等待

int waitStatus

等待状态,包含如下几个状态:

1、CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,该节点不会参与

同步状态的竞争,需要从同步队列中取消等待,节点进入该状态后将不会再变化;

2、SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态

或者被取消,将会通知后继节点,使后继节点的线程得以运行;

3、CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对

Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到同步状态

的获取中;

4、PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地传播下去;

5、初始值为0

Node prev

前驱节点,当节点加入同步队列时被设置(尾部添加)

Node next

后继节点

Node nextWaiter

等待队列中的后继节点。如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型

(独占和共享)和等待队列中的后继节点公用同一个字段

Thread thread

获取同步状态的线程

节点是构成同步队列的基础,同步器拥有首节点head和尾节点tail,没有成功获取同步状态的线程将会成为节点加入到该队列尾部,同步队列的基本结构如下图所示:

在上图中,同步器包含了两个节点引用类型,一个指向头结点,另一个指向尾节点。

入队

获取同步状态失败的线程要被构造成节点,并被加入到同步队列尾部,而这个加入队列的过程必须要保证线程安全,因此,同步器提供了一个基于CAS的设置尾节点的方法:

private final boolean compareAndSetTail(Node expect, Node update)

只有当设置成功后,当前节点才正式与之前的尾节点建立关联。添加节点的操作是通过addWaiter(Node)方法完成的,源码如下:

// 为当前线程创建一个节点并入队,然后返回这个节点
// 使用CAS算法入队并设置为尾节点
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;
    // 如果队尾不为null,则尝试插入队列
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果队尾为null,则调用enq(Node)方法插入
    enq(node);
    return node;
}

addWaiter(Node)方法主要是将当前线程构造为一个Node节点,然后入队,入队时,首先尝试的是快速入队。何为快速入队?直接把我们刚才构造的Node的前驱指针指向当前尾节点,然后通过CAS操作把我们刚才构造的node作为新的尾节点,最后再把原来老的尾节点的后继指针指向现在的新的尾节点。

快速入队的前提是这个同步队列必须先存在。如果不存在,那么只能走常规的入队操作流程,也就是进入到enq(Node)方法中。从这里我们也可以知道,其实队列的初始化在同步器的整个生命周期中只会执行一次,后续的入队操作都会按快速入队的方式入队。

enq(Node)方法源码如下:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            // 队列必须初始化,若多个线程并发执行此操作,通过CAS能保证只有一个线程执行成功
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 采用快速入队的方式入队
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

enq(Node)方法使用死循环以及CAS的方式来保证节点正确添加到同步队列中。同步器将节点加入到同步队列的过程如下图所示:


出队

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头结点的方法不需要使用CAS操作来保证线程安全,它只需要将首节点设置成原首节点的后继节点并断开原首节点的next引用即可。设置首节点的操作是通过setHead(Node)方法来完成的,源码如下:

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

设置首节点的过程如下图所示:


相关博客

AbstractQueuedSynchronizer简介

AbstractQueuedSynchronizer独占式同步状态获取与释放

AbstractQueuedSynchronizer共享式同步状态获取与释放

参考资料

方腾飞:《Java并发编程的艺术》

猜你喜欢

转载自blog.csdn.net/qq_38293564/article/details/80481242