AQS(AbstractQueuedSynchronizer)抽象队列同步器及工作原理解析

 更新时间:2022年03月25日 09:54:18   作者:聪明不喝牛奶  
AQS是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO对列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态,本文给大家详细介绍下AQS抽象队列同步器的相关知识,感兴趣的朋友一起看看吧

前言

AQS 绝对是JUC的重要基石,也是面试中经常被问到的,所以我们要搞清楚这个AQS到底是什么?骑工作原理是什么?

AQS是什么?

AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件,相比synchronized,synchronized缺少了获取锁与释放锁的可操作性,可中断、超时获取锁。

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

CLH队列:CLH(Craig, Landin, and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。通过CAS完成对State值的修改。
同步对列的内部结构及继承关系

用银行办理业务的案例模拟AQS如何进行线程管理和通知机制

1、初始化的时候,state = 0 (0 表示没有人,1表示有人),线程池也有没有人在执行,现在就是没有顾客的时候,因此第一个线程去的时候都是公平锁状态直接到窗口办理业务。

2、现在有一个线程Thread A进来那么就直接到了业务窗口去办理,并且通过CAS将state的值变成1

3、此时有一个线程Thread B 进来,通过getStatue() 方法查看到state = 1,此时ThreadA有在占用着,所以现在ThreadB线程就必须先入队等待ThreadA结束后再调用,但是由于现在队列是空的,所以ThreadB线程并不会马上进入到队列,他会先进入addWaiter() 方法到enq()这个方法中去,这个方法实质上就是一个自旋锁,在这个方法中主要的时候先要实现一个队列的初始化工作,先形成一个傀儡结点(哨兵结点)起到一个占位的作用,然后才能将ThreadB线程挂在后面。

部分源码附上:

	private Node addWaiter(Node mode) {
     Node node = new Node(Thread.currentThread(), mode);
     // Try the fast path of enq; backup to full enq on failure
     Node pred = tail;// 将tail的前指针赋值过去
     if (pred != null) { //一开始的时候并没有指向所以位null,因此条件不成立不走里面的语句
         node.prev = pred;
         if (compareAndSetTail(pred, node)) {//CAS
             pred.next = node;
             return node;
         }
     }
     enq(node);//一开始会来走这个enq()这个方法。
     return node;
 }

因为当前的ThreadB线程进入时,tail的prev指针为null所以就会进enq()方法。如下:

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

4、ThreadC线程开始入队,那么这个就是跟ThreadB是一样的,只不过是说没有了初始化那步了,直接挂在线程ThreadB上即可。

5、ThreadB 会去尝试acquireQueued()这个方法,那么第一次的时候会将哨兵结点的waitStatus = -1; 然后继续自旋,进入到if条件的下一个条件中被调用park() 阻塞掉,等待着ThreadA 线程的unpark()。这样才算真正的入列等待了。

	final boolean acquireQueued(final Node node, int arg) {
     boolean failed = true;
     try {
         boolean interrupted = false;
         for (;;) {
             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()) //第一次的时候会阻塞在前面的条件并将哨兵的waitState置为-1,第二次的话第一个条件为真,所以会走第二个判断条件,会将访问的线程给阻塞掉等待unpark();
                 interrupted = true;
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
 }
	private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this); //将访问次方法的线程给阻塞掉,等待unpark()方法唤醒
     return Thread.interrupted();
 }

6、当ThreadA线程办好业务的时候那么就会调用unlock()方法释放锁,unlock() 方法中又会调用sync.release()方法,并将当前窗口的线程变成null,state 置成0,通过调用 unparkSuccessor()方法将 傀儡结点的waitState = 0

	private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        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); //释放锁唤醒等待队列中的等待线程
    }

7、接着会调用unpark()方法将在前面等待的ThreadB线程给唤醒去窗口办业务。将head结点指向ThreadB结点,ThreadB的prev结点为null,next结点也为null,ThreadB变成空结点,此节点就成了新的哨兵结点,原来的哨兵结点被GC回收。

8、ThreadC线程出列和上面的过程是一样的。

结语

AQS的核心大致是这样,如果说我讲的你还是没有听懂的话,可以去B站看善硅谷阳哥的高频面试题那集去看看。我只是将其做了一个简化,用自己的话把这个过程复述出来,希望能对你有所帮助。

到此这篇关于AQS(AbstractQueuedSynchronizer)抽象队列同步器的文章就介绍到这了,更多相关AQS抽象队列同步器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 解决MyBatis返回结果类型为Boolean的问题

    解决MyBatis返回结果类型为Boolean的问题

    这篇文章主要介绍了解决MyBatis返回结果类型为Boolean的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • 使用java的Calendar对象获得当前日期

    使用java的Calendar对象获得当前日期

    本文给大家分享的是使用使用java的Calendar对象获得当前日期的上几个度开始、结束时间,主要思路是先获得当前季度的开始和结束日期,在当前日期的基础上往前推3个月即上个季度的开始和结束日期,十分的实用,小伙伴们可以参考下。
    2015-07-07
  • 快速入门介绍Java中强大的String.format()

    快速入门介绍Java中强大的String.format()

    这篇文章主要给大家介绍了如何快速入门介绍Java中强大的String.format()的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-03-03
  • Java ThreadLocal的详细解释

    Java ThreadLocal的详细解释

    这篇文章主要介绍了快速了解Java中ThreadLocal类,介绍了ThreadLocal 是什么,ThreadLocal的作用,ThreadLocal 原理等相关内容,具有一定参考价值,需要的朋友可以了解下。
    2021-11-11
  • idea激活ActivateJrebel热部署的方法详解

    idea激活ActivateJrebel热部署的方法详解

    这篇文章主要介绍了idea激活ActivateJrebel热部署的方法,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • java7钻石语法知识点总结

    java7钻石语法知识点总结

    在本篇文章里小编给大家整理的是关于java7钻石语法的相关知识点内容,有需要的朋友们参考下。
    2019-11-11
  • Android中Handler引起的内存泄露问题解决办法

    Android中Handler引起的内存泄露问题解决办法

    这篇文章主要介绍了Android中Handler引起的内存泄露问题解决办法,本文讲解了导致内存泄露的情景,并给出了修改后的代码,需要的朋友可以参考下
    2015-01-01
  • SpringMVC通过拦截器实现IP黑名单

    SpringMVC通过拦截器实现IP黑名单

    这篇文章主要为大家详细介绍了SpringMVC通过拦截器实现IP黑名单,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-08-08
  • J2EE验证码图片如何生成和点击刷新验证码

    J2EE验证码图片如何生成和点击刷新验证码

    这篇文章主要介绍了J2EE如何生成验证码图片如何生成,如何点击刷新验证码的相关方法,感兴趣的小伙伴们可以参考一下
    2016-04-04
  • Java得到一个整数的绝对值,不使用任何判断和比较语句,包括API

    Java得到一个整数的绝对值,不使用任何判断和比较语句,包括API

    Java得到一个整数的绝对值,不使用任何判断和比较语句,包括API
    2009-09-09

最新评论