一文疏通 AQS 到 ReentrantLock

一文疏通 AQS 到 ReentrantLock

AQS 是什么

AQS 全称是AbstractQueuedSynchronizer, 是juc 下一个核心的抽象类,用于构建各种同步器和锁

比如我们熟悉的 ReentrantLock、ReadWriteLock、CountDownLatch等等是基于AQS

工作原理

下面这张图很清晰了反映了 AQS 的工作原理

当线程获取锁时,即试图对state变量做修改,如修改成功则获取锁;

如修改失败则包装为节点挂载到队列中,等待持有锁的线程释放锁并唤醒队列中的节点

首先在AQS 里面,有几个核心的组成

● state: 共享资源的状态

● 以Node节点组成的双端队列------CLH

● 两个维护队列的Node节点head 和 tail

AQS 基本的属性(源码)

java 复制代码
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    //头节点
    private transient volatile Node head;
    //尾节点
    private transient volatile Node tail;
    //同步状态
    private volatile int state;   
 
     static final class Node {
            //节点状态
            volatile int waitStatus;
            //前驱节点
            volatile Node prev;
            //后继节点
            volatile Node next;
            //当前节点所代表的线程
            volatile Thread thread;
            //等待队列使用时的后继节点指针
            Node nextWaiter;
    }

}    

这里的Node 的 waitStatus 需要留意一下:

该字段共有5种取值:

● CANCELLED = 1。节点引用线程由于等待超时或被打断时的状态。

● SIGNAL = -1。后继节点线程需要被唤醒时的当前节点状态。当队列中加入后继节点被挂起(block)时,其前驱节点会被设置为SIGNAL状态,表示该节点需要被唤醒。

● CONDITION = -2。当节点线程进入condition队列时的状态。(见ConditionObject)

● PROPAGATE = -3。仅在释放共享锁releaseShared时对头节点使用。(见共享锁分析)

● 0。节点初始化时的状态

独占锁分析

acquire(int)

java 复制代码
public final void acquire(int arg) {
    // tryAcquire需实现类处理
    // 如获取资源成功,直接返回
    if (!tryAcquire(arg) && 
        // 如获取资源失败,将线程包装为Node添加到队列中阻塞等待
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如阻塞线程被打断
        selfInterrupt();
}

acquire核心为tryAcquire、addWaiter和acquireQueued三个函数,其中tryAcquire需具体子类实现。 每当线程调用acquire时都首先会调用tryAcquire,失败后才会挂载到队列,因此acquire实现默认为非公平锁(后到的线程可能会通过这里的tryAcquire 直接拿到锁)

addWaiter将线程包装为节点 node,尾插式加入到队列中,如队列为空,则会添加一个空的头节点。值得注意的是addWaiter中的enq方法,通过CAS+自旋的方式处理尾节点添加冲突。

java 复制代码
// sync队列中的结点在独占且忽略中断的模式下获取(资源)
final boolean acquireQueued(final Node node, int arg) {
    // 标志
    boolean failed = true;
    try {
        // 中断标志
        boolean interrupted = false;
        for (;;) { // 无限循环
            // 获取node节点的前驱结点
            final Node p = node.predecessor(); 
            if (p == head && tryAcquire(arg)) { // 前驱为头节点并且成功获得锁
                setHead(node); // 设置头节点
                p.next = null; // help GC
                failed = false; // 设置标志
                return interrupted; 
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())//
                //shouldParkAfterFailedAcquire只有当该节点的前驱结点的状态为SIGNAL时,才可以对该结点所封装的线程进行park操作。否则,将不能进行park操作。
                //parkAndCheckInterrupt首先执行park操作,即禁用当前线程,然后返回该线程是否已经被中断
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);// 直接把这个节点置空了
    }
}

acquireQueue在线程节点加入队列后判断是否可再次尝试获取资源,如不能获取则将其前驱节点标志为SIGNAL状态(表示其需要被unpark唤醒)后,则通过park进入阻塞状态

release(int)

java 复制代码
public final boolean release(int arg) {
    if (tryRelease(arg)) { // 释放成功
        // 保存头节点
        Node h = head; 
        if (h != null && h.waitStatus != 0) // 头节点不为空并且头节点状态不为0
            unparkSuccessor(h); //释放头节点的后继结点
        return true;
    }
    return false;
}
private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {//如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

release流程较为简单,尝试释放成功后,即从头结点开始唤醒其后继节点,如后继节点被取消,则转为从尾部开始找阻塞的节点将其唤醒。阻塞节点被唤醒后,即进入acquireQueued中的for(;;)循环开始新一轮的资源竞争

总结

上述内容仔细看完,已经可以理解基本的AQS 工作原理了,但是我们的实现类需要实现两个try 的方法(因为默认是直接抛异常,这可不行),所以下面的ReentrantLock 其实并没有添加很多的锁的细节,基本上用的还是AQS 这套锁的流程,无疑就是进行了公平与非公平的分类,以及实现对应的try 方法

ReentrantLock的类结构

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

ReentrantLock 实现了 Lock 接口,有三个内部类,其中 Sync 继承自 AQS ,而后两者继承自 Sync ,它们都继承了 AQS 的能力。

公平锁与非公平锁

构造函数

java 复制代码
//生成一个公平锁
static Lock lock = new ReentrantLock(true);
//生成一个非公平锁
static Lock lock = new ReentrantLock(false);
static Lock lock = new ReentrantLock();//默认参数就是false


public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();//FairSync表示公平锁,NonfairSync表示非公平锁
}

具体实现tryAcquire

其公平的核心方法:

java 复制代码
    /* 判断等待队列中是否存在等待中的线程 */
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        // 判断是否存在先等待的线程 具体分析如下
        return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
    }

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

tryRelease

java 复制代码
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        // 获取独占锁的线程才可以释放线程
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // state == 0 表示所有锁已释放
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

总结

ReentrantLock 实现了 Lock 接口,有三个内部类,其中 Sync 继承自 AQS ,而后两者继承自 Sync ,它们都继承了 AQS 的能力。

本质上来说 ReentrantLock 的底层原理就是 AQS 。

在 Sync 的两个子类 FairSync 和 NonfairSync 分别是公平锁策略和非公平锁策略的实现(公平锁多了一个检查是否有其他等待线程的条件)。然后实现了不同的 tryAcquire(int acquires) ,从而在线程尝试获取锁时,执行不同的策略

参考:

Java 多线程并发 【10】ReentrantLock - 掘金 (juejin.cn)

10分钟从源码级别搞懂AQS(AbstractQueuedSynchronizer) - 掘金 (juejin.cn)

【后端面经-Java】公平锁和加锁流程 - 掘金 (juejin.cn)

相关推荐
百炼成神 LV@菜哥1 小时前
记HttpURLConnection下载图片
java·开发语言·后端
且随疾风前行.1 小时前
技术成神之路:设计模式(十六)代理模式
设计模式·代理模式
高高要努力2 小时前
SpringBoot-全局处理异常,时间格式,跨域,拦截器,监听器
java·spring boot·spring
Code豪客2 小时前
Java常用三类定时器快速入手指南
java·开发语言·后端·spring
阿乾之铭2 小时前
Lombok 在 IntelliJ IDEA 中的使用步骤
java·ide·intellij-idea
printf_8242 小时前
Android 长按文本弹出输入框
android·java·开发语言
李少兄2 小时前
使用 IntelliJ IDEA 连接到达梦数据库(DM)
java·数据库·intellij-idea
2401_857297913 小时前
秋招内推2025--招联金融
java·前端·算法·金融·求职招聘
武昌库里写JAVA3 小时前
机器学习笔记 - week6 -(十一、机器学习系统的设计)
java·开发语言·算法·spring·log4j
颜淡慕潇4 小时前
【数据库】Java 中 MongoDB 使用指南:步骤与方法介绍
java·数据库·sql·mongodb