09-Java多线程-2、AQS-CLH队列

AQS - CLH

一、CLH

  • 在上一篇文章简单介绍了AQS,知道了AQS内部会维护一个同步队列,而这个队列就是CLH(Craig, Landin, and Hagersten)队列,它是一个FIFO的双向队列,AQS依赖它来完成同步状态的管理。当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

  • CLH locks ( Craig, Landin, and Hagersten, CLH锁 ) 是自旋锁,能够确保无饥饿,能够保证先来先服务的公平性。

二、Node

  • Node是AbstractQueuedSynchronizer 的静态内部类。是内部队列保存的元素,它封装了一个线程的状态信息,如果线程需要
    阻塞排队,那么就会封装成一个Node节点进入FIFO队列。

2.1 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
         */
        //后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使
        // 后继节点的线程得以运行,后继节点需要unpark
        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;

        /**
         * Status field, taking on only the values:
         * SIGNAL:     The successor of this node is (or will soon be)
         * blocked (via park), so the current node must
         * unpark its successor when it releases or
         * cancels. To avoid races, acquire methods must
         * first indicate they need a signal,
         * then retry the atomic acquire, and then,
         * on failure, block.
         * 后继节点被阻塞或者即将被阻塞,因此当前节点在释放同步状态或者取消的时候,必须unpark他的
         * 后继节点,为了避免竞争,获取同步状态的方法必须先预测是否需要发信号,然后再尝试获取同步状
         * 态,然后再是成功或者阻塞
         * CANCELLED:  This node is cancelled due to timeout or interrupt.
         * Nodes never leave this state. In particular,
         * a thread with cancelled node never again blocks.
         * 节点因为超时或者中断取消了等待,节点会一直保持这个状态,并且不会再次阻塞
         * CONDITION:  This node is currently on a condition queue.
         * It will not be used as a sync queue node
         * until transferred, at which time the status
         * will be set to 0. (Use of this value here has
         * nothing to do with the other uses of the
         * field, but simplifies mechanics.)
         * 当前线程会在一个Condition队列,除非状态变化否则它不会进入同步队列,状态变化的时候,会被设置为0
         * PROPAGATE:  A releaseShared should be propagated to other
         * nodes. This is set (for head node only) in
         * doReleaseShared to ensure propagation
         * continues, even if other operations have
         * since intervened.
         * 一个共享模式释放应该要无条件的传播到其他节点,这个只会被头节点在共享模式释放的时候设置,
         * 来确保传播的连续
         * 0:          None of the above
         * 不是上面的值,就是0
         * <p>
         * The values are arranged numerically to simplify use.
         * Non-negative values mean that a node doesn't need to
         * signal. So, most code doesn't need to check for particular
         * values, just for sign.
         * <p>
         * The field is initialized to 0 for normal sync nodes, and
         * CONDITION for condition nodes.  It is modified using CAS
         * (or when possible, unconditional volatile writes).
         * waitStatus代表等待状态,值可能为SIGNAL/CANCELLED/CONDITION/PROPAGATE/0 这几种;
         */
        volatile int waitStatus;

        /**
         * 前驱节点,当节点添加到同步队列时被设置(尾部添加)
         */
        volatile Node prev;

        /**
         * 后继节点
         */
        volatile Node next;

        /**
         * The thread that enqueued this node.  Initialized on
         * construction and nulled out after use.
         * 封装的线程对象
         */
        volatile Thread thread;

        /**
         * 1.如果保存的是一个特殊的值(SHARED = new Node()),就表示是共享模式
         * 2.如果不是特殊值,nextWaiter表示Condition队列的下一个节点
         * condition队列是独占模式,因此等待在condition的节点的队列使用一个linked  queue保存节点即可
         */
        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
         * 返回当前节点的前驱节点,Null检查可以被省略,但是有助于VM
         */
        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;
        }
    }

2.2 Node字段解析

  • 通过表格来表示各个字段的含义:
字段含义
waitStatus等待状态,用来控制线程的阻塞和唤醒,并且可以避免不必要的#park(…)和#unpark(…) 方法。他的值为:SIGNAL/CANCELLED/CONDITION/PROPAGATE/0 ,0位初始状态。
prev 和 next分别指向当前Node的前驱和后继节点
head 和 tail分别指向当前Node链表的头结点和尾节点
threadNode节点对应的线程
nextWaiterCondition队列的下一个等待的线程(共享模式则是一个特殊值SHARED = new Node())

三、Node操作方法

3.1 addWaiter入列

3.1.1 代码
  • 入队操作如果从单线程的角度来看,就是将新的Node加到队列尾部,并将原本的尾节点的后继指针指向新的Node,但是AQS中是多线程的操作,
    因此在设置尾节点的时候分2个步骤,首先读取tail是node1,步骤2将尾节点从node1设置为node,步骤1和2之间很可能其他线程已经将尾节点从
    node1修改为node2了,因此是不安全的,需要使用CAS设置,如果我尝试将尾节点由node1设置为node的时候,发现期望值并不是node1而已经变成
    了node2,那我就再循环一次,再读取一次tail发现是node2,我再CAS设置的时候,就会成功了。(不成功就一直自旋直到成功)
    /**
     * 将当前线程按照指定的模式,创建一个Node对象,并加入队列
     */
    private Node addWaiter(Node mode) {
        //1.创建Node节点对象
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //2.快速尝试,添加新节点为尾节点
        if (pred != null) {
            //3.设置新 Node 节点为原尾节点(即把原尾节点作为Node的前驱)
            node.prev = pred;
            //4.CAS设置新的尾节点(因为是双向链表,除了将node的前驱指向原来的为节点之外,还需要将node设置为新的尾节点)
            if (compareAndSetTail(pred, node)) {
                //5.成功,原尾节点的下一个节点为新节点,这里就相当于把原来的为节点的后继指针指向node,到这才算一个完整的双向指针
                pred.next = node;
                return node;
            }
        }
        //6.如果快速尝试失败,那就进入enq方法进行多次尝试,直到成功
        enq(node);
        return node;
    }
    
    /**
     * 自旋设置Node为尾节点,直到成功
     */
    private Node enq(final Node node) {
        //自旋设置Node为尾节点,直到成功
        for (; ; ) {
            Node t = tail;
            //1.若原尾节点为空,说明队列是空的,那就初始化,头尾节点是一样的
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //2.将新节点的前驱指向原来的队尾节点
                node.prev = t;//A
                //4.修改尾节点为node
                if (compareAndSetTail(t, node)) { //B
                    //5.设置成功才会将原来的队尾的后继指针指向新的队尾节点node,
                    t.next = node;
                    return t;
                }
                //6.如果设置失败(比如代码A和B之间其他线程设置了尾节点,那么CAS会失败),就会进
                // 入下一次循环,下一次就会重新获取Node t = tail;并设置node.prev = t;,就会
                // 成功了,即使不成功,又会继续循环,其实只要AB之间没有被其他线程设置就会成功,
                //CAS这样其实不会进入死循环,因为每次尝试都会获取到新的tail,然后CAS设置新的
                // tail为node,但是可能会有线程自旋太久
            }
        }
    }
    
  • 代码中发现addWaiter方法和enq的逻辑其实类似,都是将线程封装的Node节点保存进队列,加在双向链表的队列尾部。里面用到了CAS自旋(for循环)保证线程安全,另外需要对链表有一定的了解,图片
    如下。
3.1.2 入列示意图

image

3.2 setHead出列

  • CLH 同步队列遵循 FIFO,首节点的线程释放同步状态后,将会唤醒它的下一个节点(Node.next)。而后继节点将会在获取同步状态成功时,将自己设置为首节点( head )。
    这个过程相对简单,head执行该节点并断开原首节点的next和当前节点的prev即可。注意,在这个过程是不需要使用 CAS 来保证的,因为只有一个线程能够成功获取到同步
    状态,并不是并发操作。
3.2.1 代码
     /**
     * 将node设置为队列的头结点
     */
    private void setHead(Node node) {
        //1.设置node成为首节点,因此head指针指向node
        head = node;
        //2.自己已经获得了同步状态,因此thread引用可以置为null,避免不必要的信号和遍历
        node.thread = null;
        //3.前驱指针也可以置null了,因为自己已经是队首
        node.prev = null;
    }
    
  • 因为设置头结点的操作不是并发的,因此比较简单,就是将head指针指向对应node,并将node内部不需要的字段置为null。
3.2.2 出列示意图

image

四、小结

  • 本文我们主要分析了AQS在对线程进行排队处理的过程中封装线程的数据结构Node,大致了解到其在Node中封装了很多状态信息,这些信息是将线程在FIFO队列中进行同步控制的关键。
  • 同步过程中的关键操作是入队和出队,入队出队都是将封装后的Node节点加到FIFO队列或者从FIFO队列中移除,入队过程需要考虑线程安全,因此使用了CAS,出队过程是将一个新的节
    点设置为头结点,因为只有一个线程可以修改头结点,所以不是并发操作,因此比较简单。
  • 本文我们并没有涉及更上层的代码分析,这是最底层的对FIFO队列的操作,至于何时该入队何时不该入队,这些逻辑我们还没有进行分析,有了这部分的基础,后续我们在进行上层的
    分析的时候,就可以不关心FIFO的队列细节,无非就是将一个Node扔进队列和取出队列的过程,AQS的代码比较复杂,我们需要一步一步的分解学习。后续的文章会进行AQS中同步状态变
    量读取的相关代码解读。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值