「JUC篇」之 AbstractQueuedSynchronizer详解

68 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

觉得对你有益的小伙伴记得点个赞+关注

后续完整内容持续更新中

希望一起交流的欢迎发邮件至javalyhn@163.com

1. 什么是AbstractQueuedSynchronizer

1.1 字面意思

通常地:AbstractQueuedSynchronizer简称为AQS

抽象的队列同步器

image.png

1.2 技术解释

是用来构建锁或者其它同步器组件的重量级基础框架整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量 表示持有锁的状态

image.png

CLH:Craig、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO

2. AQS为什么是JUC内容中最重要的基石

2.1 和AQS有关的内容

image.png

ReentrantLock

image.png

CountDownLatch

image.png

ReentrantReadWriteLock

image.png

Semaphore

image.png

2.2 锁和同步器的关系

锁是面向锁的使用者,定义了程序员和锁交互的使用层API,隐藏了实现细节,调用即可

同步器是面向锁的实现者,比如Java并发大神DougLee,提出统一规范并简化了锁的实现, 屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。

3. AQS的作用

加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要队列

抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。

image.png

4. 初步理解AQS

我们看一下官方解释

image.png

有阻塞就需要排队,实现排队必然需要队列

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

image.png

5. AQS内部体系架构

5.1 AQS自身

AQS的int变量:这是AQS的同步状态State成员变量

image.png

好比银行业务的受理窗口,假设如果是0,就是自由状态可以办理;大于等于1就说明有人正在占用窗口,需要等待。

AQS的CLH队列:CLH队列(三个大牛的名字组成),为一个双向队列

image.png

好比银行候客区的等待顾客

小总结:有阻塞就需要排队,要排队必然需要队列 (state变量+CLH双向队列)

5.2 内部类Node(在AQS类内部)

Node的int变量:Node的等待状态waitState成员变量

Node的int变量和AQS自身的int变量完全是两码事!!!

image.png

好比银行排队等待区其他顾客(其他线程)的等待状态,队列中每一个排队的个体就是Node

Node的内部结构以及属性说明

image.png

image.png

6. AQS同步队列基本结构

image.png

CLH:Craig、Landin and Hagersten 队列,是个单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO)

7. 从ReentrantLock开始解读AQS

7.1 Lock接口

Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的

7.2 ReentrantLock的原理

image.png

7.3 从lock方法看公平与非公平

image.png

image.png

image.png

7.4 公平锁与非公平锁在源码上有什么区别

image.png

可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件: hasQueuedPredecessors() hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法

这里我先讲非公平锁,以非公平锁为突破口,hasQueuedPredecessors()这个方法最后讲(不难)

8. 非公平锁lock() 源码详解

8.1 非公平锁走起,方法lock()

对比公平锁和非公平锁的 tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors() hasQueuedPredecessors() 中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

image.png

8.2 lock()

image.png

8.3 acquire()

image.png

acquire()有三大流程

  1. tryAcquire()
  2. addWaiter()
  3. acquireQueued

8.4 tryAcquire(arg)

该函数的作用是尝试获得一个许可,对于AQS来说这是一个没有实现的抽象类,他的实现交给子类, image.png

image.png

image.png

该函数有两个返回值

true 结束

false 继续推执行下一个方法

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //拿到state状态值(返回AQS自身的int变量值)
    int c = getState();
    //如果是0就表示没有其他线程持有锁,则当前线程有机会对它赋值
    if (c == 0) {
        // 如果赋值成功,则表示当前线程持有锁(此处采用CAS)
        // 对state加1,表示当前线程第一次持有锁,注意acquires=1
        if (compareAndSetState(0, acquires)) {
            // 将当前线程赋值到exclusiveOwnerThread字段 
            // 其他地方可以通过该字段判断是哪个线程在持有锁
            setExclusiveOwnerThread(current);
            //返回true,表示加锁成功
            return true;
        }
    }
    // 如果不是0,则说明有线程占用锁了,那么判断占有锁的线程是不是 
    // 当前线程,如果是,则可重入(ReentrantLock)
    else if (current == getExclusiveOwnerThread()) {
        // 因为再次持有锁(重入进来的),所以此处对state赋值 
        // state是几,就表示当前是第几次再次获得锁
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 如果以上都不是,则没有获取到锁
    return false;
}

8.5 addWaiter(Node.EXCLUSIVE)

如果没有获取到锁就会走到这一步

image.png

这个方法的注释: 创建一个入队node为当前线程,Node.EXCLUSIVE是独占锁,Node.SHARED是共享锁

private Node addWaiter(Node mode) {
    //创建一个入队node为当前线程
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 尝试enq的快速路径;故障时备份到完整enq
    Node pred = tail;
    // 如果pred(尾节点)不为空
    if (pred != null) {
        // 将node的前驱结点prev设置为pred
        node.prev = pred;
        // 用CAS设置当前node为尾结点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 不存在tail节点,进入该方法
    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说明已经初始化过,就往队列后添加节点
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的。

8.6 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

当addWaiter插入节点后,调用该方法进行阻塞

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 如果node是第二个节点,并且线程当前持有锁
            // 那么就将node变成第一个节点(head节点),源head直接GC回收掉
            // 就是将队列中的每一个元素往前挪动1个位置
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 如果没有获取到锁,则调用LockSupport.park方法挂起,并放到队列的最后一个(tail)位置
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前驱结点的状态
    int ws = pred.waitStatus;
    // 如果是SIGNAL状态,及等待被占用的资源释放,直接返回true
    // 准备继续调用parkAndCheckInterrupt
    if (ws == Node.SIGNAL)
        return true;
    // ws大于0说明是CANCELLED状态
    if (ws > 0) {
        // 循环判断前驱结点的前驱结点是否也为CANCELLED状态,忽略该状态的节点,重新连接队列
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
       // 将当前节点的前驱结点设置为SIGNAL状态,用于后续被唤醒操作
       // 程序第一次执行到这里返回false,还会进行第二次循环,最终从代码第七行返回
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

如果前驱节点的 waitStatus 是 SIGNAL状态,即 shouldParkAfterFailedAcquire 方法会返回 true 程序会继续向下执行 parkAndCheckInterrupt 方法用于将当前线程挂起

private final boolean parkAndCheckInterrupt() {
    // 线程挂起,不会再向下执行
    LockSupport.park(this);
    // 根据park方法API描述,程序在下面三种情况会继续向下执行
    // 1 被unpack
    // 2 被中断(interrupt)
    // 3 其他不合逻辑的返回才会继续向下执行
    
    // 因上述三种情况执行至此,返回当前线程的中断状态,并且清空中断标志
    // 如果由于被中断,该方法会返回true
    return Thread.interrupted();
}

8.7 公平锁lock()的hasQueuedPredecessors()

public final boolean hasQueuedPredecessors() {
    Node h, s;
    // 如果头节点不为空
    if ((h = head) != null) {
    	// 如果第2个节点为空,或者第2个节点不为空但是取消了等待(>0)
        if ((s = h.next) == null || s.waitStatus > 0) {
            s = null; // traverse in case of concurrent cancellation
            //从尾部向头部遍历,找到离头部最近的,并且waitStatus<=0的节点,用s保存这个节点
            for (Node p = tail; p != h && p != null; p = p.prev) {
                if (p.waitStatus <= 0)
                    s = p;
            }
        }
       	// 如果s不是空并且s中的线程不是当前线程,则说明s中保存的线程正在等待获取锁,所以返回true,表示有其他线程等待
        if (s != null && s.thread != Thread.currentThread())
            return true;
    }
    // 如果头节点为空,说明队列里没任何线程,进一步说明没有其他线程在等待,直接返回false
    return false;
}

9. 建议

AQS还是有难度的,需要多复习!