AbstractQueuedSynchronizer超详细原理解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介:  今天我们来研究学习一下AbstractQueuedSynchronizer类的相关原理,java.util.concurrent包中很多类都依赖于这个类所提供队列式同步器,比如说常用的ReentranLock,Semaphore和CountDownLatch等。

 今天我们来研究学习一下AbstractQueuedSynchronizer类的相关原理,java.util.concurrent包中很多类都依赖于这个类所提供队列式同步器,比如说常用的ReentranLockSemaphoreCountDownLatch等。
 为了方便理解,我们以一段使用ReentranLock的代码为例,讲解ReentranLock每个方法中有关AQS的使用。

ReentranLock示例

 我们都知道ReentranLock的加锁行为和Synchronized类似,都是可重入的锁,但是二者的实现方式确实完全不同的,我们之后也会讲解Synchronized的原理。除此之外,Synchronized的阻塞无法被中断,而ReentrantLock则提供了可中断的阻塞。下面的代码是ReentranLock的函数,我们就以此为顺序,依次讲解这些函数背后的实现原理。

ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock();

公平锁和非公平锁

ReentranLock分为公平锁和非公平锁,二者的区别就在获取锁机会是否和排队顺序相关。我们都知道,如果锁被另一个线程持有,那么申请锁的其他线程会被挂起等待,加入等待队列。理论上,先调用lock函数被挂起等待的线程应该排在等待队列的前端,后调用的就排在后边。如果此时,锁被释放,需要通知等待线程再次尝试获取锁,公平锁会让最先进入队列的线程获得锁。而非公平锁则会唤醒所有线程,让它们再次尝试获取锁,所以可能会导致后来的线程先获得了锁,则就是非公平。

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

 我们会发现FairSyncNonfairSync都继承了Sync类,而Sync的父类就是AbstractQueuedSynchronizer(后续简称AQS)。但是AQS的构造函数是空的,并没有任何操作。
 之后的源码分析,如果没有特别说明,就是指公平锁。

lock操作

ReentranLocklock函数如下所示,直接调用了synclock函数。也就是调用了FairSynclock函数。

    //ReentranLock
    public void lock() {
        sync.lock();
    }
    //FairSync
    final void lock() {
        //调用了AQS的acquire函数,这是关键函数之一
        acquire(1);
    }

 我们接下来就正式开始AQS相关的源码分析了,acquire函数的作用是获取同一时间段内只能被一个线程获取的量,这个量就是抽象化的锁概念。我们先分析代码,你慢慢就会明白其中的含义。

public final void acquire(int arg) {
    // tryAcquire先尝试获取"锁",获取了就不进入后续流程
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //addWaiter是给当前线程创建一个节点,并将其加入等待队列
        //acquireQueued是当线程已经加入等待队列之后继续尝试获取锁.
        selfInterrupt();
}

tryAcquireaddWaiteracquireQueued都是十分重要的函数,我们接下来依次学习一下这些函数,理解它们的作用。

//AQS类中的变量.
private volatile int state;
//这是FairSync的实现,AQS中未实现,子类按照自己的需要实现该函数
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //获取AQS中的state变量,代表抽象概念的锁.
    int c = getState();
    if (c == 0) { //值为0,那么当前独占性变量还未被线程占有
        //如果当前阻塞队列上没有先来的线程在等待,UnfairSync这里的实现就不一致
        if (!hasQueuedPredecessors() && 
            compareAndSetState(0, acquires)) {
            //成功cas,那么代表当前线程获得该变量的所有权,也就是说成功获得锁
            setExclusiveOwnerThread(current);
            // setExclusiveOwnerThread将本线程设置为独占性变量所有者线程
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        //如果该线程已经获取了独占性变量的所有权,那么根据重入性
        //原理,将state值进行加1,表示多次lock
        //由于已经获得锁,该段代码只会被一个线程同时执行,所以不需要
        //进行任何并行处理
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //上述情况都不符合,说明获取锁失败
    return false;
}

 由上述代码我们可以发现,tryAcquire就是尝试获取那个线程独占的变量state。state的值表示其状态:如果是0,那么当前还没有线程独占此变量;否在就是已经有线程独占了这个变量,也就是代表已经有线程获得了锁。但是这个时候要再进行一次判断,看是否是当前线程自己获得的这个锁,如果是,就增加state的值。

ReentranLock获得锁

 这里有几点需要说明一下,首先是compareAndSetState函数,这是使用CAS操作来设置state的值,而且state值设置了volatile修饰符,通过这两点来确保修改state的值不会出现多线程问题。然后是公平锁和非公平锁的区别问题,在UnfairSyncnonfairTryAcquire函数中不会在相同的位置上调用hasQueuedPredecessors来判断当前是否已经有线程在排队等待获得锁。

 如果tryAcquire返回true,那么就是获取锁成功;如果返回false,那么就是未获得锁,需要加入阻塞等待队列。我们下面就来看一下addWaiter的相关操作。

等待锁的阻塞队列

 将保存当前线程信息的节点加入到等待队列的相关函数中涉及到了无锁队列的相关算法,由于在AQS中只是将节点添加到队尾,使用到的无锁算法也相对简单。真正的无锁队列的算法我们等到分析ConcurrentSkippedListMap时在进行讲解。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //先使用快速入列法来尝试一下,如果失败,则进行更加完备的入列算法.
    //只有在必要的情况下才会使用更加复杂耗时的算法,也就是乐观的态度
    Node pred = tail; //列尾指针
    if (pred != null) {
        node.prev = pred; //步骤1:该节点的前趋指针指向tail
        if (compareAndSetTail(pred, node)){ //步骤二:cas将尾指针指向该节点
            pred.next = node;//步骤三:如果成果,让旧列尾节点的next指针指向该节点.
            return node;
        }
    }
    //cas失败,或在pred == null时调用enq
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) { //cas无锁算法的标准for循环,不停的尝试
        Node t = tail;
        if (t == null) { //初始化
            if (compareAndSetHead(new Node())) 
              //需要注意的是head是一个哨兵的作用,并不代表某个要获取锁的线程节点
                tail = head;
        } else {
            //和addWaiter中一致,不过有了外侧的无限循环,不停的尝试,自旋锁
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

 通过调用addWaiter函数,AQS将当前线程加入到了等待队列,但是还没有阻塞当前线程的执行,接下来我们就来分析一下acquireQueued函数。

等待队列节点的操作

 由于进入阻塞状态的操作会降低执行效率,所以,AQS会尽力避免试图获取独占性变量的线程进入阻塞状态。所以,当线程加入等待队列之后,acquireQueued会执行一个for循环,每次都判断当前节点是否应该获得这个变量(在队首了)。如果不应该获取或在再次尝试获取失败,那么就调用shouldParkAfterFailedAcquire判断是否应该进入阻塞状态。如果当前节点之前的节点已经进入阻塞状态了,那么就可以判定当前节点不可能获取到锁,为了防止CPU不停的执行for循环,消耗CPU资源,调用parkAndCheckInterrupt函数来进入阻塞状态。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) { //一直执行,直到获取锁,返回.
            final Node p = node.predecessor(); 
            //node的前驱是head,就说明,node是将要获取锁的下一个节点.
            if (p == head && tryAcquire(arg)) { //所以再次尝试获取独占性变量
                setHead(node); //如果成果,那么就将自己设置为head
                p.next = null; // help GC
                failed = false;
                return interrupted;
                //此时,还没有进入阻塞状态,所以直接返回false,表示不需要中断调用selfInterrupt函数
            }
            //判断是否要进入阻塞状态.如果`shouldParkAfterFailedAcquire`
            //返回true,表示需要进入阻塞
            //调用parkAndCheckInterrupt;否则表示还可以再次尝试获取锁,继续进行for循环
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //调用parkAndCheckInterrupt进行阻塞,然后返回是否为中断状态
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) //前一个节点在等待独占性变量释放的通知,所以,当前节点可以阻塞
        return true;
    if (ws > 0) { //前一个节点处于取消获取独占性变量的状态,所以,可以跳过去
        //返回false
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //将上一个节点的状态设置为signal,返回false,
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); //将AQS对象自己传入
    return Thread.interrupted();
}

阻塞和中断

 由上述分析,我们知道了AQS通过调用LockSupportpark方法来执行阻塞当前进程的操作。其实,这里的阻塞就是线程不再执行的含义,通过调用这个函数,线程进入阻塞状态,上述的lock操作也就阻塞了,等待中断或在独占性变量被释放。

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);//设置阻塞对象,用来记录线程被谁阻塞的,用于线程监控和分析工具来定位
    UNSAFE.park(false, 0L);//让当前线程不再被线程调度,就是当前线程不再执行.
    setBlocker(t, null);
}

 关于中断的相关知识,我们以后再说,就继续沿着AQS的主线,看一下释放独占性变量的相关操作吧。
ReentrantLock未获得阻塞,加入队列

unlock操作

 与lock操作类似,unlock操作调用了AQSrelase方法,参数和调用acquire时一样,都是1。

public final boolean release(int arg) {
    if (tryRelease(arg)) { 
    //释放独占性变量,起始就是将status的值减1,因为acquire时是加1
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//唤醒head的后继节点
        return true;
    }
    return false;
}

 由上述代码可知,release就是先调用tryRelease来释放独占性变量。如果成功,那么就看一下是否有等待锁的阻塞线程,如果有,就调用unparkSuccessor来唤醒他们。

protected final boolean tryRelease(int releases) {
    //由于只有一个线程可以获得独占先变量,所以,所有操作不需要考虑多线程
    int c = getState() - releases; 
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) { //如果等于0,那么说明锁应该被释放了,否则表示当前线程有多次lock操作.
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

 我们可以看到tryRelease中的逻辑也体现了可重入锁的概念,只有等到state的值为0时,才代表锁真正被释放了。所以独占性变量state的值就代表锁的有无。当state=0时,表示锁未被占有,否在表示当前锁已经被占有。

private void unparkSuccessor(Node node) {
    .....
     //一般来说,需要唤醒的线程就是head的下一个节点,但是如果它获取锁的操作被取消,或在节点为null时
     //就直接继续往后遍历,找到第一个未取消的后继节点.
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

 调用了unpark方法后,进行lock操作被阻塞的线程就恢复到运行状态,就会再次执行acquireQueued中的无限for循环中的操作,再次尝试获取锁。

ReentrantLock释放锁并通知阻塞线程恢复执行

后记

 有关AQSReentrantLock的分析就差不多结束了。不得不说,我第一次看到AQS的实现时真是震惊,以前都认为SynchronizedReentrantLock的实现原理是一致的,都是依靠java虚拟机的功能实现的。没有想到还有AQS这样一个背后大Boss在提供帮助啊。学习了这个类的原理,我们对JUC的很多类的分析就简单了很多。此外,AQS涉及的CAS操作和无锁队列的算法也为我们学习其他无锁算法提供了基础。知识的海洋是无限的啊!

相关文章
|
20天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
38 3
|
8天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
22 1
|
13天前
|
数据采集 存储 编解码
一份简明的 Base64 原理解析
Base64 编码器的原理,其实很简单,花一点点时间学会它,你就又消除了一个知识盲点。
42 3
|
1月前
|
前端开发 Java 应用服务中间件
21张图解析Tomcat运行原理与架构全貌
【10月更文挑战第2天】本文通过21张图详细解析了Tomcat的运行原理与架构。Tomcat作为Java Web开发中最流行的Web服务器之一,其架构设计精妙。文章首先介绍了Tomcat的基本组件:Connector(连接器)负责网络通信,Container(容器)处理业务逻辑。连接器内部包括EndPoint、Processor和Adapter等组件,分别处理通信、协议解析和请求封装。容器采用多级结构(Engine、Host、Context、Wrapper),并通过Mapper组件进行请求路由。文章还探讨了Tomcat的生命周期管理、启动与停止机制,并通过源码分析展示了请求处理流程。
|
28天前
|
开发框架 缓存 前端开发
electron-builder 解析:你了解其背后的构建原理吗?
本文首发于微信公众号“前端徐徐”,详细解析了 electron-builder 的工作原理。electron-builder 是一个专为整合前端项目与 Electron 应用的打包工具,负责管理依赖、生成配置文件及多平台构建。文章介绍了前端项目的构建流程、配置信息收集、依赖处理、asar 打包、附加资源准备、Electron 打包、代码签名、资源压缩、卸载程序生成、安装程序生成及最终安装包输出等环节。通过剖析 electron-builder 的原理,帮助开发者更好地理解和掌握跨端桌面应用的构建流程。
60 2
|
10天前
|
供应链 安全 分布式数据库
探索区块链技术:从原理到应用的全面解析
【10月更文挑战第22天】 本文旨在深入浅出地探讨区块链技术,一种近年来引起广泛关注的分布式账本技术。我们将从区块链的基本概念入手,逐步深入到其工作原理、关键技术特点以及在金融、供应链管理等多个领域的实际应用案例。通过这篇文章,读者不仅能够理解区块链技术的核心价值和潜力,还能获得关于如何评估和选择适合自己需求的区块链解决方案的实用建议。
32 0
|
21天前
|
前端开发 JavaScript UED
axios取消请求CancelToken的原理解析及用法示例
axios取消请求CancelToken的原理解析及用法示例
68 0
|
24天前
|
存储 缓存 数据处理
深度解析:Hologres分布式存储引擎设计原理及其优化策略
【10月更文挑战第9天】在大数据时代,数据的规模和复杂性不断增加,这对数据库系统提出了更高的要求。传统的单机数据库难以应对海量数据处理的需求,而分布式数据库通过水平扩展提供了更好的解决方案。阿里云推出的Hologres是一个实时交互式分析服务,它结合了OLAP(在线分析处理)与OLTP(在线事务处理)的优势,能够在大规模数据集上提供低延迟的数据查询能力。本文将深入探讨Hologres分布式存储引擎的设计原理,并介绍一些关键的优化策略。
76 0
|
30天前
|
SQL 分布式计算 大数据
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(一)
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(一)
36 0
|
30天前
|
SQL 分布式计算 算法
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(二)
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(二)
67 0

推荐镜像

更多