前言

相信大家对Java中的Lock锁应该不会陌生,比如ReentrantLock,锁主要是用来解决解决多线程运行访问共享资源时的线程安全问题。那你是不是很好奇,这些Lock锁api是如何实现的呢?本文就是来探讨一下这些Lock锁底层的AQS(AbstractQueuedSynchronizer)到底是如何实现的。

本文是基于ReentrantLock来讲解,ReentrantLock加锁只是对AQS的api的调用,底层的锁的状态(state)和其他线程等待(Node双向链表)的过程其实是由AQS来维护的

加锁

我们先来看看加锁的过程,先看源码,然后模拟两个线程来加锁的过程。

上图是ReentrantLock的部分实现。里面有一个Sync的内部类的实例变量,这个Sync内部类继承自AQS,Sync子类就包括公平锁和非公平锁的实现。说白了其实ReentrantLock是通过Sync的子类来实现加锁。

我们就来看一下Sync的非公平锁的实现NonfairSync。

重写了它的lock加锁方法,在实现中因为是非公平的,所以一进来会先通过cas尝试将AQS类的state参数改为1,直接尝试加锁。如果尝试加锁失败会调用AQS的acquire方法继续尝试加锁。

假设这里有个线程1先来调用lock方法,那么此时没有人加锁,那么就通过CAS操作,将AQS中的state中的变量由0改为1,代表有人来加锁,然后将加锁的线程设置为自己如图。

那么此时有另一个线程2来加锁,发现通过CAS操作会失败,因为state已经被设置为1了,线程线程2就会设置失败,那么此时就会走else,调用AQS的acquire方法继续尝试加锁。

进入到acquire会先调用tryAcquire再次尝试加锁,而这个tryAcquire方法AQS其实是没有什么实现的,会调用到NonfairSync里面的tryAcquire,而tryAcquire实际会调用到Sync内部类里面的nonfairTryAcquire非公平尝试加锁方法。

 

先获取锁的状态,判断锁的状态是不是等于0,等于0说明没人加锁,可以尝试去加,如果被加锁了,就会走else if,else if会判断加锁的线程是不是当前线程,是的话就给state 加 1,代表当前线程加了2次锁,就是可重入锁的意思(所谓的可重入就是代表一个线程可以多次获取到锁,只是将state 设置为多次,当线程多次释放锁之后,将state 设置为0才代表当前线程完全释放了锁)。

这里所有的条件假设都不成立。也就是线程2尝试加锁的时候,线程1并没有释放锁,那么这个方法就会返回false。

接下来就会走到addWaiter方法,这个方法很重要,就是将当前线程封装成一个Node,然后将这个Node放入双向链表中。addWaiter先根据指定模式创建指定的node节点,因为ReentrantLock是独占模式,所以传进去的EXCLUSIVE,这里通过当前线程和模式传入,初始化一个双向node节点,获取最后一个节点,根据最后一个节点是否存在来操作当前节点的父级。如果尾节点不存在会去调用enq去初始化

放入链表中之后如图。

然后调用acquireQueued方法

这个方法一进来也会尝试将当前节点去加锁,然后如果加锁成功就将当前节点设置为头节点,最后将当前线程中断,等待唤醒。

线程2进来的时候,刚好线程2的前一个节点是头节点,但是不巧的是调用tryAcquire方法,还是失败,那么此时就会走shouldParkAfterFailedAcquire方法,这个方法是在线程休眠之前调用的,很重要,我们来看看干了什么事。

判断当前节点的父级节点的状态,如果父级状态是-1,则代表当前线程可以被唤醒了。如果父级的状态为取消状态(什么叫非取消状态,就是tryLock方法等待了一些时间没获取到锁的线程就处于取消状态)就跳过父级,寻找下一个可以被唤醒的父级,然后绑定上节点关系,最后将父级的状态更改为-1。也就说,线程(Node)加入队列之后,如果没有获取到锁,在睡眠之前,会将当前节点的前一个节点设置为非取消状态的节点,然后将前一个节点的waitStatus设置为-1,代表前一个节点在释放锁的时候需要唤醒下一个节点。这一步骤主要是防止当前休眠的线程无法被唤醒。这一切设置成功之后,就会返回true。

接下来就会调用parkAndCheckInterrupt

,这个方法内部调用LockSupport.park方法,此时当前线程就会休眠。

到这一步线程2由于没有获取到锁,就会在这里休眠等待被唤醒。

来总结一下加锁的过程。

线程1先过来,发现没人加锁,那么此时就会加上锁。此时线程2过来,在线程2加锁的过程中,线程1始终没有释放锁,那么线程2就不会加锁成功(如果在线程2加锁的过程中线程1始终释放锁,那么线程2就会加锁成功),线程2没有加锁成功,就会将自己当前线程加入等待队列中(如果没有队列就先初始化一个),然后设置前一个节点的状态,最后通过LockSupport.park方法,将自己这个线程休眠。

如果后面还有线程3,线程4等等诸多的先过来,那么这些线程都会按照前面线程2的步骤,将自己插入链表后面再休眠。

释放锁

ok,说完加锁的过程之后,我们来看看释放锁干了什么。

ReentrantLock的unlock其实是调用AQS的release方法,我们直接进入release方法,看看是如何实现的

进入tryRelease方法,看一下Sync的实现

其实很简单,就是判断锁的状态,也就是加了几次锁,然后减去释放的,最后判断释放之后,锁的状态是不是0(因为可能线程加了多次锁,所以得判断一下),是的话说明当前这个锁已经释放完了,然后将占有锁的线程设置为null,然后返回true,

然后就会走接下来的代码。

就是判断当前链表头节点是不是需要唤醒队列中的线程。如果有链表的话,头结点的waitStatus肯定不是0,因为线程休眠之前,会将前一个节点的状态设置为-1,上面加锁的过程中有提到过。

接下来就会走unparkSuccessor方法,successor代表继承者的意思,见名知意,这个方法其实就会唤醒当前线程中离头节点最近的没有状态为非取消的线程。然后调用LockSupport.unpark,唤醒等待的线

然后线程就会从阻塞的那里苏醒过来,继续尝试获取锁。

我再次贴出这段代码。

获取到锁之后,就将头节点设置成自己。

对应我们的例子,就是线程1释放锁之后,就会唤醒在队列中线程2,先成2获取到锁之后,就会将自己前一个节点(也就是头节点)从链表中移除,将自己设置成头节点。该方法就会跳出死循环。

到这里,释放锁的过程就讲完了,其实很简单,就是当线程完完全全释放了锁,会唤醒当前链表中的没有取消的,离头结点最近的节点(一般就是链表中的第二个节点),然后被唤醒的节点就会获取到锁,将头节点设置为自己。

总结

相信看完这篇文章,大家对AQS的底层有了更深层次的了解。AQS其实就是内部维护一个锁的状态变量state和一个双向链表,加锁成功就将state的值加1,加锁失败就将自己当前线程放入链表的尾部,然后休眠,等待其他线程完完全全释放锁之后将自己唤醒,唤醒之后会尝试加锁,加锁成功就会执行业务代码了。

到这里本文就结束了,如果你有什么疑问欢迎私信告诉我。

如果觉得这篇文章对你有所帮助,还请帮忙点赞、关注、转发一下,码字不易,非常感谢!

如果你想联系我,欢迎关注我的个人微信公众号 三友的java日记,每天都会发布技术性的文章,期待与你一起进步。

一文带你看懂Java中的Lock锁底层AQS到底是如何实现的的更多相关文章

  1. 轻松搞懂Java中的自旋锁

    前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...

  2. Java中的Lock锁

    Lock锁介绍: 在java中可以使用 synchronized 来实现多线程下对象的同步访问,为了获得更加灵活使用场景.高效的性能,java还提供了Lock接口及其实现类ReentrantLock和 ...

  3. 带你看懂LayoutInflater中inflate方法

    关于inflate问题,我想很多人多多少少都了解一点,网上也有很多关于这方面介绍的文章,但是枯燥的理论或者翻译让很多小伙伴看完之后还是一脸懵逼,so,我今天想通过三个案例来让小伙伴彻底的搞清楚这个东东 ...

  4. 三个案例带你看懂LayoutInflater中inflate方法两个参数和三个参数的区别

    关于inflate参数问题,我想很多人多多少少都了解一点,网上也有很多关于这方面介绍的文章,但是枯燥的理论或者翻译让很多小伙伴看完之后还是一脸懵逼,so,我今天想通过三个案例来让小伙伴彻底的搞清楚这个 ...

  5. 从基础到实践,一文带你看懂HashMap

    摘要:HashMap是一个用于存储Key-Value键值对的集合,它是面试中经常问到的一个知识点. HashMap是面试中经常问到的一个知识点,也是判断一个候选人基础是否扎实的标准之一,因为通过Has ...

  6. 【转】三个案例带你看懂LayoutInflater中inflate方法两个参数和三个参数的区别

    关于inflate参数问题,我想很多人多多少少都了解一点,网上也有很多关于这方面介绍的文章,但是枯燥的理论或者翻译让很多小伙伴看完之后还是一脸懵逼,so,我今天想通过三个案例来让小伙伴彻底的搞清楚这个 ...

  7. 一文带你看懂cookie,面试前端不用愁

    本文由云+社区发表 在前端面试中,有一个必问的问题:请你谈谈cookie和localStorage有什么区别啊? localStorage是H5中的一种浏览器本地存储方式,而实际上,cookie本身并 ...

  8. 一文带你看懂WebSocket 的原理?为什么可以实现持久连接?

    工作之余在知乎上偶然看到一篇回帖,瞬间觉得之前看的那么多资料都不及这一篇让我对 websocket 的认知深刻易懂,之前看总完总觉得一知半解云里雾里.所以与大家共同分享一下一起学习.比较喜欢这种博客理 ...

  9. 一文带你看懂HarmonyOS应用上架

    大家一直以来都很关心如何上架HarmonyOS应用,现在它来了!它终于来了! 我们为大家梳理了HarmonyOS应用从创建.调试到上架的流程和注意事项,希望能为你的上架之旅带来帮助! 一.创建/添加应 ...

随机推荐

  1. 基于nodejs中实现跨域的方法

    一般情况下跨域是通过ajax的方式请求数据,通过js在不同的域之间进行数据传输或者通信: 只有通过ajax方式获取请求的时候才会有跨域问题需要解决: 例如在本地模拟两个服务端. 一个服务端去通过aja ...

  2. Win7运行net5 wpf条件

    Win7运行net5 wpf条件 win7 sp1 dotnet-runtime-5 vc_redist KB2999226 KB4457144 Tips:官网条件最后一个最坑爹,KB2533623不 ...

  3. OllyDbg---寄存器

    寄存器 寄存器的概念和作用 寄存器是CPU内部的高速存储单元,访问速度比常规内存快很多. 处理器在执行程序时,需要一个助手,当执行一条指令时,比如将两个内存单元中存放的内容相加,处理器需要先把其中一个 ...

  4. python---希尔排序的实现

    def shell_sort(alist): """希尔排序""" n = len(alist) gap = n // 2 # 插入算法执行 ...

  5. 时间篇之centos6下修复的ntp操作(ntpd和ntpdate两个服务区别)

    系统采样,本采样和命令都是在centos6.4的系统中进行 主要比较centos7和centos6之间的差异,因为大部分都开始采用centos7但是有些老系统还采用centos6,这样我们就需要熟悉c ...

  6. mysql-cluster-gpl-7.5.10-linux-glibc2.12-x86_64.tar.gz (有必要解释一下)

    大部分软件我们接触的时候会发现,起的名称有点怪异,所以我觉得有必要解释一下. 比如: mysql-cluster-gpl-7.5.10-linux-glibc2.12-x86_64.tar.gz 名称 ...

  7. 结合手工注入编写一个SQL盲注脚本——以SQLi-Labs less16为例

    一.分析测试注入点 1.抓包,查看响应数据包 2.先随便输入一个账号密码,再测试万能密码 1") or 1=1 -- # 3.发现响应数据包的Content-Length字段值不同.错误状态 ...

  8. babel 的介绍及其配置

    vue/cli -- babel Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其 ...

  9. Centos7 搭建 Socks 服务

    Centos7 搭建 Socks 服务 一丶拿到一个动态拨号的服务器还不能使用网络得先打开: pppoe-start 二丶安装命令汇总: 通过yum安装ss5 依赖包: yum install gcc ...

  10. codeforces标签设置【codeforces内操作, 非浏览器操作】

    直接干货~ 明确需求,关闭标签 步骤: 1.选中上方PROBLEM SET 2.找到Settings  第一个选项是展示未accepted的标签, 第二个选项是隐藏已accepted的标签 官方标签设 ...