本文导读

  • 生活中案例场景介绍
  • 联想到 AQS 到底是什么
  • AQS 的设计初衷
  • 揭秘 AQS 底层实现
  • 最后的总结

当你在学习某一个技能的时候,是否曾有过这样的感觉,就是同一个技能点学完了之后,过了一段时间,如果你没有任何总结,或者是不经常回顾,遗忘的速度是非常之快的。

忘记了之后,然后再重新学,因为已经间隔了一段时间,再次学习又当做了新的知识点来学。这种状态如此反复,浪费了相同的时间,但学习效果却收效甚微。

每当遇到这种情况,我们可以停下来,思考一下。对于某一个技术知识点理解起来不是那么好懂的时候,或者是学习起来有点吃力的时候,咱们可以尝试找找生活中的例子来联想下。

因为技术源于生活。

找到一个合适的生活案例,然后结合你自己做笔记总结和动手实践的过程。定期的去回顾一下,慢慢的就会理解的更加透彻。

1、生活中案例场景介绍

今天我们就举一个生活中的例子来理解下并发底层的AQS。

大家如果去过某些大医院的话,就能知道,由于互联网的快速发展,医院的挂号、交费、取药的流程都是比较方便的,交费也可以使用支付宝、微信支付了,而不用带现金了。

医生开完单子,交费完成 ,单子上都会有一个长条二维码,可以直接在取药的地方自助扫码,叫号系统自动分配取药窗口,然后你在关注下指定窗口等待着叫号就可以了,叫到你的时候再过去取药,而不需要一直在等待着。

我们用一张图来直观的感受下:

file

这里面涉及到了几个角色:

1)药房,提供取药窗口的,内部有自助取药机或人工取药

2)取药叫号系统,当用户扫码药单后,自动录入到该系统中

3)取药用户

接下来咱们细化下取药流程。

当取药用户在自助机器上扫码时,可以直观的看下下面的流程图:

取药流程图1

第一个用户是程序猿,因为有多个自助扫码机,他一看二维码就知道咋回事了,所以第一个在自助机上扫码完成,可以优先第一个去取药窗口(State窗口)。

此时叫号系统的药单队列中还没有其他人,程序猿扫码后,就可以直接去窗口等待着取药了。

接下来,本来是张大爷和王大妈看着先前程序猿的操作,也跟着在自助机上来回扫码一把,由于不大懂扫哪里,扫了半天也没有个反应,老头此时有点懵 : (。

后来热心的程序猿看到了,给指点了一下 : ),帮助顺利的扫码完成。

再看下面这个流程图:

取药流程图2

正好,张大爷和王大妈的取药单,也被分配到跟程序猿同一个取药窗口中 ,此时只能排队了,按照他们的扫码顺序排队,如上图所示。

当程序猿取药完成,叫号系统会自动呼叫下一位用户,即队列中的排在首节点的张大爷,自助取药机收到消息会自动给张大爷取药。此时,王大妈还是要等一会。后面的用户 CCC 扫码完成后,会继续放到药单队列中,药单队列是按照 FIFO,也就是谁先扫码谁就在前面,所以 CCC 排在王大妈的后面。

再看下面的流程图:

取药流程图3

张大爷还在等待取药过程中,王大妈也知道下一个可能就是她了,所以王大妈会时不时的,抬头看看叫号窗口是否显示了自己的名字。
此时,王大妈可以稍微在等待区休息一会,等待系统叫号就可以了。

2、联想到 AQS 到底是什么

其实,上面的场景介绍中,在医院里是很常见的。那么这个场景对应的,我们可以联想到 Java 中的并发编程。

如果没有中间的叫号系统来做控制,如果医院没有限制,很多用户要么一拥而上没有秩序的乱挤,要么就有秩序的都在窗口站着排成长队等待着。

所以中间的叫号系统解决了很多问题,解决了很多取药用户的有序性、安全性,而且不需要用户一直等着,用户线程无阻塞,当收到系统通知信号后,用户再继续执行取药动作。

这个生活中的例子,可以很好的联想到 Java 中我们常用的,并发包的底层技术:AQS (AbstractQueuedSynchronizer)队列同步器(简称同步器)。

就像我们举得例子中的提到的几个角色,有很多用户(理解为用户线程),有共享资源(取药窗口)。在用户线程和共享资源之间,是通过中间系统来协调控制的,这里面就会涉及的概念。

是用来控制多个线程访问共享资源的方式。一个锁能防止多个线程对共享资源的同时访问,有些锁也允许多个线程并发访问共享资源,比如读写锁。

在 Java 中经常使用的锁是 synchronized,synchronized 会隐式的获得锁,但它必须是先获得锁再释放锁。这种方式简化了同步的管理,但扩展性不如 Lock 显示的获得锁和释放锁更加灵活。

synchronized 和 Lock 锁之间的区别:

synchronized和Lock锁区别

从性能上来讲,当并发量高、竞争激烈的场景下,Lock 锁会较 synchronized 性能上表现的
更稳定些。反之,当并发量不高的情况下,synchronized 有分级锁的优势,因此两者性能差不多,synchronized 相对来说使用上更加简单,不用考虑手工释放锁。

直观感受下两者的性能对比:

性能对比

Lock 显示的锁使用,因为使用上更加灵活,这得益于其底层基础同步框架的实现机制,它就是 AQS。

如下图所示:

多线程访问共享资源

上述图中列出了多个并发包中的类,每一个并发工具类解决的问题场景不同,但是其底层同步框架基本都是使用的 AQS 来实现的。

3、AQS 的设计初衷

Java 大佬考虑并发底层使用 AQS 的设计思想初衷,就是为了能够抽象出来统一的同步协调处理器,设计好顶层结构,作为并发包构建的基本骨架,该骨架里封装了多线程的入队/出队、线程阻塞/唤醒等一系列复杂的操作。Java SDK 中面向开发者针对不同需求场景提供了多个并发包工具。

尽管,提供的这些并发包的实现方式是不一样的,但都是基于顶层抽象出来的 AQS 所定义的统一接口基础上,然后部分定制逻辑延迟到子类去自行实现。同时,部分定义的方法中是按照既定的顺序执行的,由此,我们也能够想到,AQS 使用了模板方法模式。

在上一节图中提到的几个并发包中,我们来简单介绍下实现场景。

多线程独占式并发工具:

1)ReentrantLock

可重入锁,同一时刻仅允许一个线程访问,所以可以称作 独占锁,线程可以重复获取同一把锁。

多线程共享式并发工具:

1)ReentrantReadWriteLock