深入理解AQS的CLH队列

前言

我们知道,AQS定义了两种队列,同步等待队列(CLH队列)和条件等待队列(CONDITION队列),在学习AQS的过程中对这两个队列总是有种雾蒙蒙的感觉,到底是怎么入队、阻塞、唤醒、出队的?到底是怎么保证并发安全的?本文以ReentrantLock来对CLH队列进行深度剖析,详细介绍CLH的结构,入队、出队过程,阻塞和唤醒过程,以及其中的并发安全问题

本文属于进阶分析,需要熟悉AQS的基本属性和方法

一、CLH队列的结构和初始化

1.结构

如下图所示,是一个双向链表,由node组成,head和tail分别指向头节点和尾节点。node有pre、next、thread、waitStatus这几个属性,队列初始时新建一个node节点,其pre、next、thread为null,waitStatus为0。每个节点的waitStatus是后续节点是否能被唤醒的信号(后面会详细介绍)
深入理解AQS的CLH队列_第1张图片
CLH队列用到的3种waitStatus:0:初始,1:取消,-1:需要唤醒

//表示线程已取消:由于在同步队列中等待的线程等待超时或中断
//需要从同步队列中取消等待,节点进入该状态将不会变化(即要移除/跳过的节点)
static final int CANCELLED =  1;
//表示后继节点处于park,需要唤醒:后继节点的线程处于park,而当前节点
//的线程如果进行释放或者被取消,将会通知(signal)后继节点。
static final int SIGNAL = -1;
//节点的等待状态,初始值为0
volatile int waitStatus;

2.初始化

在入队的时候,如果tail为null,说明队列还未初始化,需要初始化。新建一个node,node中的thread、pre和next均为null,cas将head指向该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;
                }
            }
        }
    }

二、阻塞和唤醒时,CLH队列入队和出队过程

先说一般情况,我们以如下代码为例,t0线程先获取到了锁,0.5s后t1进入CLH队列进行等待,又0.5s后t2进入CLH队列进行等待,2s后t0线程执行完释放锁,t1获取锁,又2s后t1线程执行完释放锁,t2获取锁,再2s后t2执行完释放锁

private static ReentrantLock lock = new ReentrantLock(true);
    public static void reentrantLock() throws InterruptedException {
   
        String threadName = Thread.currentThread().getName();
        lock.lock();
        log.info("Thread:{},加锁成功!",threadName);
        Thread.sleep(2000);
        lock.unlock();
        log.info("Thread:{},锁退出同步块",threadName);
    }

    public static void main(String[] args) {
   
        Thread t0 = new Thread(new Runnable() {
   
            @Override
            public void run() {
   
                try 

你可能感兴趣的:(并发编程,java)