同事crud三年了跟我说还没看过AQS源码,我反手就给了他一巴掌----AQS源码详解

面试场景:

面试官:平时工作中有用到锁吗?
应聘者:有。
面试官:…
应聘者:…
(沉默3秒)
面试官:怎么用的?
应聘者:把想要同步的地方加上synchronized关键字。
面试官:synchronized怎么实现的?
应聘者:编译器会把加了synchronized关键字的代码加上_monitorenter_monitorexit来表明锁的开始和结束。
面试官:你确定吗?
应聘者:呃…不确定
面试官:锁的种类有哪些?
应聘者:偏向锁、自旋锁、轻量级锁和重量级锁。
面试官:轻量级锁是如何实现锁的重入的?锁升级的流程和条件是什么?
应聘者:…
面试官:那ReentrantLock知道吗?
应聘者:知道…
面试官:它和synchronized有什么区别?
应聘者:synchronized是基于JVM层面实现的,ReentrantLock基于JDK层面。ReentrantLock使用起来更灵活…它可以显示的加锁和解锁并且效率比synchronized更高…
面试官:你确定ReentrantLock比synchronized效率更高吗?
应聘者:…
面试官:ReentrantLock怎么实现的?
应聘者:它是基于抽象队列同步器AQS实现的,底层加锁用的是CAS。
面试官:ReentrantLock的加锁流程是什么?
应聘者:…
面试官:AQS是如何唤醒阻塞线程的?
应聘者:…
面试官:…
应聘者:…
(沉默3秒)
面试官:今天面试暂时到这里吧,你回去等通知。


synchronized和AQS几乎是所有程序员面试过程当中的高频知识点,对于很多crud几年的程序员来说,要是平时没留意这些知识点,可能面试被问到还真答不上来。关于synchronized,本人推荐看看这篇文章: 死磕Synchronized底层实现–概论。本文将着重从源码级别带领大家一窥AQS的究竟,让我们开始吧!

什么是AQS

AQS是Abstract Queued Synchronizer的简称,也就是抽象队列同步器。抽象是抽象谁?同步又是如何同步?别急,我们先整体来看一下AQS的家族成员:
AQS家族成员
首先我们需要明确的是,队列是以内部类的形式维护在AQS中的,并且是一个双向的链表。另外ReentrantLock、CountDownLatch、Semaphore和线程池都用到了AQS,有意思的是,它们无一例外都是采用内部类的形式去继承AQS,然后这些子类又分离出了FairSync和NoFairSync分别对应公平锁非公平锁的不同实现。我们以ReentrantLock的源码为例,在其源码中按F12查看成员信息:
reentrantlock成员
可以看到Sync重写了AQS的tryRelease方法,而其两个子类FairSync和NoFairSync均重写了tryAcquire方法。讲到这里,需要补充说明一下,tryRelease方法是用来释放锁的,而tryAcquire是用来尝试获取锁的,这两个方法被子类重写,也就意味着AQS中的这两个方法没有提供默认实现其实现不满足子类需求。是不是这样呢?我们接着看一下AQS的这两个方法:

  • tryAcquire
    在这里插入图片描述
  • tryRelease
    在这里插入图片描述
    好家伙,这俩方法二话不说一上来就直接抛异常,很明显就是让子类去重写嘛。
节点Node的状态

在AQS中维护的队列的节点Node是有等待状态的,我们来看一下都有哪些状态:

  • CANCELLED:1 代表节点已经被取消,不会被其它节点调度。
  • SIGNAL:-1 代表后继节点需要被当前节点唤醒。当新的节点加入队列时,会更改其前继节点的状态为signal。
  • CONDITION:-2 表明当前节点处于Condition中等待,就是使用Condition对象调用await的时候会将节点设置为此状态。而在调用signal和signalAll的时候会把节点加入到阻塞队列中。
  • PROPAGATE:-3 表明锁是可传播的,也就是解锁的时候不仅把后继节点唤醒,也可能唤醒其它节点。
  • 默认值:0 初始化的时候默认是该值。
    除了等待状态,node还有两种模式:共享锁模式(shared mode)独占锁模式(exclusive mode),所谓的共享模式就是可以一次性唤醒多个线程;而独占模式就是一次只会唤醒一个阻塞线程。

加锁流程

非公平锁单线程的加锁流程

为了方便阅读,我们写一个用ReentrantLock加锁的小程序(报红是阿里规范插件提示):
demo
我们直接debug程序跑一下
step0

  • Step1:直接调用当前实例的lock方法,可以看到里面又调用了sync.acquire,sync就是ReentrantLock中的那个继承了AQS的内部类。
    step1
  • Step2,第一步中调用的acquire方法实际上调用的是父类AQS的acquire方法。
    我们可以先大概看一下这个判断逻辑:
    !tryAccquire代表没有拿到锁
    acquireQueued表示将当前线程加入阻塞队列中,当中还用addWaiter来创建一个新的等待节点,而Node.EXCLUSIVE代表创建的是一个独占锁。
    step2
  • step3,由于ReentrantLock在的无参构造器默认使用的是非公平锁,并且NonfairSync重写了tryAcquire方法,所以这一步就直接跳到了非公平锁的实现上。可以看到这一步直接返回了nonfairTryAcquire,这个方法其实是Sync已经默认提供的方法。
    在这里插入图片描述
  • step4,跳转至Sync的nonfairTryAcquire实现,详细步骤画在图里了,我主要想讲一下图中第三步的CAS操作
    step4
    如果我们在第三步中继续step into会看到:
    在这里插入图片描述
    这是AQS中的一个类型为VarHandle成员STATE,这是一个什么东西呢?我们直接看STATE如何定义的:
    state
    我们专注于STATE,其中l.findVarHandle(AbstractQueuedSynchronizer.class, “state”, int.class)的语义是在指定类中查找名为state且类型是int的属性。返回的对象就是那个属性本身的一个引用,我们打开AQS的属性搜一下state,可以看到:
    在这里插入图片描述
    我刚才说,AQS本身维护了一个队列,那么我怎么知道某个时刻是否有线程在给队列上锁呢?用的就是这个状态值state去表示。那么VarHandle又是啥呢?我们打开VarHandle的源码,在头部的注释中看到这么一段话:
    var作用
    大致含义是,提供了对成员变量的动态强类型引用,允许变量进行一些基于底层内存屏障实现的读写操作和CAS操作。这个类是JDK9之后出现的,其目的是代替一部分unsafe类的操作。原来如此,我们用VarHandle引用了某个变量后,便可以对其进行CAS操作了,我们可以看到,AQS除了引用了STATE,还对队列中的头节点和尾节点进行了引用(划重点,待会要考)
  • step5,由于没有其它线程加锁,所以修改state值成功,设置队列中拥有锁的线程为当前线程,然后返回true
    加锁成功
多线程情况下非公平锁的加锁流程

我们修改一下主程序,开启两个线程,然后追踪第二个线程的加锁流程:
demo2

  • step1,还是最终调用到AQS中的acquire方法
    在这里插入图片描述
  • step2,线程1已经修改了state的值,所以当线程2修改state状态失败,tryAcquire最终返回false
    demostep2
  • step3,获取锁失败后,!tryAcquire为true,所以继续执行第二个逻辑,将当前线程加入队列中阻塞
    在这里插入图片描述
  • step4,要把当前线程加入到队列中先得新建一个等待节点,addWaiter就是用来创建这个节点的,我们来看一下新增等待节点的逻辑
    demo2step4
    由于没有初始化队列,所以拿到的队尾是空,于是首先进入初始化队列的逻辑,我们继续往下看它是如何初始化这个队列的:
    addnode
    初始化完成队列之后,我们会得到一个只有一个节点的队列,然后会继续执行刚才的循环逻辑,这里的用到CAS去操作队尾节点,这也是为什么之前用VarHandle去指向这个队尾元素的引用
    cycle
    -step5,判断节点是否需要添加至阻塞队列
    step5
    在这里插入图片描述
    我们主要看一下shouldParkAfterFailedAcquire这个方法:
    parkAfter
    更改了前置节点的状态为signal后会继续执行循环,这一次就会将当前线程阻塞了:
    在这里插入图片描述
    在这里插入图片描述
公平锁的加锁流程

公平锁实际上流程和非公平锁是一样的,只是公平锁在执行tryAcquire的时候看了一眼队列中是否有其它等待的线程,有的话就加入队列,没有的话就CAS修改state的值,我们看一眼源码:
在这里插入图片描述
然后是判断队列是否为空的逻辑:
在这里插入图片描述

小结

上面的流程可以用下方的流程图概括:
整体流程

解锁流程

解锁流程相对简单:
step1,执行Sync的release方法,这里公平锁和非公平锁的释放逻辑是一样的
在这里插入图片描述

  • step2,CAS操作修改state的值
    在这里插入图片描述
  • step3,如果队列不为空,唤醒队列中的其它线程
    在这里插入图片描述

总结

以上就是整个ReentrantLock的加锁流程,当然,还有很多东西没有讲到,比如说AQS中还有另外一个内部类CondtionObject,它用来维护等待队列的唤醒/等待逻辑,当我们使用Condtion的await方法的时候,在队列中增加的是一个waitstatus是CONDITION的节点,并且Node中会有另外一个属性nextWaiter标记下一个等待的节点。当然,其实这和加锁的流程大同小异,如果读者感兴趣,可以自行阅读这部分源码。

看到这里,各位读者对AQS的执行流程应该有了大致的认识。AQS抽象的是谁,其实是抽象的加锁解锁的逻辑,而AQS本身已经为我们封装好了对于阻塞队列和等待队列的各种调用。

其实笔者在写这篇文章之前已经初步看过了AQS的源码,但真当我提笔想描述清楚这些流程的时候,发现自己还有很多地方认识不足。另外这里可能会存在一个问题,就是如果线程在release的时候抛出了异常,队列中的其它线程是不会被唤醒的,当然,JDK层面的release应该不会出错,我们在写业务代码的时候应当注意这一点,养成在finally中写unlock的好习惯。

希望本文能够帮助正在学习并发编程的各位小伙伴,如若文中有不对之处,欢迎指正!

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值