JUC学习(八):AQS的CLH队列

目录

一.简介

二.Node类

三.CLH队列


一.简介

AQS是JUC的核心,无论是信号量、CDL还是可重入锁,背后都有AQS的影子。这些类的同步过程一般如下:

JUC学习(八):AQS的CLH队列_第1张图片

tryAcquire和tryRelease过程很好理解,就是CAS地修改AQS的state值,关键是doAcquire和doRelease如何管理众多线程的状态,又如何决定哪个线程可以获得锁。答案就是,AQS在其内部管理了一个链表,所有的线程都会被添加到这个链表中进行管理,这个链表由于使用了(改进的)CLH锁,在公平实现中又具有先进先出特性(非公平实现当然就是优先队列),因此被称为CLH队列。

二.Node类

首先回顾一下doAcquireInterruptibly方法的代码:

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        ...
    }

可以看到,在方法最开始,首先调用了addWaiter方法,这个方法就是将当前线程添加到等待队列的过程,对链表都所了解的都能轻易看懂:

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

可见AQS是将线程包装成Node再入队的。Node是AQS的一个内部类,其定义并不复杂:

    static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;

        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;

        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() {   
        }

        Node(Thread thread, Node mode) {     
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { 
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

最核心的成员变量就是thread、waitStatus、nextWaiter,thread保存了节点对应的线程;waitStatus用于标记节点状态;nextWaiter则决定了当前获取/释放锁的模式,有SHARED、QUEUED、EXCLUSIVE三种,例如信号量就工作在共享模式下(其acquire方法实际调用了acquireSharedInterruptibly方法)。

Node节点的状态有如下五种:

状态 解释
CANCELLED 当前节点已经取消等待(超时或中断),该状态下的节点不会进入其他状态,也不会再阻塞。
SIGNAL 后继结点已经或即将阻塞,当前节点需要唤醒后继结点。后继结点获取锁的时候,必须先收到SIGNAL,才能调用tryAcquire(如果再次失败就会重新阻塞)
CONDITION 该节点正处于Condition队列中(ReentrantLock的Condition),因为等待condition.signalAll()而阻塞
PROPAGATE 仅用于共享模式下、释放锁时的队列头节点,用于向整个队列传播锁释放信号
0 以上四种情形之外的状态,一般是新建的节点

三.CLH队列

在AQS类文件的开头,作者添加了很长一段注释,向开发者解释CLH队列,以及AQS对CLH队列的使用。

AQS里面的CLH队列是CLH同步锁的一种变形。其主要从两方面进行了改造:节点的结构与节点等待机制。

在结构上,AQS类引入了头结点和尾节点,他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关:

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

从addWaiter可以看到,CLH队列的入队实际上就是把当前节点设置为队尾,那么相应的,出队就是处理队首节点。

在Node类内,还有对前驱节点、后继节点的引用。前驱结点主要用在取消等待的场景:当前节点出队后,需要把后继结点连接到前驱结点后面;后继结点的作用是避免竞争,在doRelease方法中,当前节点出队后,会让自己的后继结点接替自己获取锁,有了明确的继承关系,就不会出现竞争了。

在等待机制上由原来的自旋改成阻塞+唤醒,以doAcquireInterruptibly为例,parkAndCheckInterrupt方法使用LockSupport令当前线程阻塞,直到收到信号被唤醒后,进入下一轮自旋:

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        ...
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        ...
    }

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

可见CLH队列的优点如下:

  • 相比于纯自旋,自旋+阻塞+唤醒的机制能够很好地兼顾性能与效率
  • 同步操作只涉及头节点、尾节点、当前节点、前驱结点、后继结点,对整个队列影响很小,明显优于整体加锁或者分段加锁
  • 通过后继结点机制,明确了获得锁的顺序(除非出现信号量申请许可过多无法获取锁这种情况,此时只需要传播状态,继续向下寻找合适节点继承锁即可),避免了竞争

你可能感兴趣的:(Java)