1.5w字,30图带你彻底掌握 AQS!(建议收藏)

前言AQS( AbstractQueuedSynchronizer )是一个用来构建锁和同步器(所谓同步 , 是指线程之间的通信、协作)的框架 , Lock 包中的各种锁(如常见的 ReentrantLock, ReadWriteLock), concurrent它包中的各种同步器(如 CountDownLatch, Semaphore, CyclicBarrier)都是基于 AQS 来构建 , 所以理解 AQS 的实现原理至关重要 , AQS 也是面试中区分候选人的常见考点 , 我们务必要掌握 , 本文将用循序渐进地介绍 AQS , 相信大家看完一定有收获 。 文章目录如下

  1. 锁原理 - 信号量 vs 编程
  2. AQS 实现原理
  3. AQS 源码剖析
  4. 如何利用 AQS 自定义一个互斥锁
锁原理 - 信号量 vs 管程在并发编程领域 , 有两大核心问题:互斥与同步 , 互斥即同一时刻只允许一个线程访问共享资源 , 同步 , 即线程之间如何通信、协作 , 一般这两大问题可以通过信号量和管程来解决 。
信号量信号量(Semaphore)是操作系统提供的一种进程间常见的通信方式 , 主要用来协调并发程序对共享资源的访问 , 操作系统可以保证对信号量操作的原子性 。 它是怎么实现的呢 。
  • 信号量由一个共享整型变量 S 和两个原子操作 PV 组成 , S 只能通过 P 和 V 操作来改变
  • P 操作:即请求资源 , 意味着 S 要减 1 , 如果 S < 0, 则表示没有资源了 , 此时线程要进入等待队列(同步队列)等待
  • V 操作: 即释放资源 , 意味着 S 要加 1 ,如果 S 小于等于 0 , 说明等待队列里有线程 , 此时就需要唤醒线程 。
示意图如下
1.5w字,30图带你彻底掌握 AQS!(建议收藏)文章插图
信号量机制的引入解决了进程同步和互斥问题 , 但信号量的大量同步操作分散在各个进程中不便于管理 , 还有可能导致系统死锁 。 如:生产者消费者问题中将P、V颠倒可能死锁(见文末参考链接) , 另外条件越多 , 需要的信号量就越多 , 需要更加谨慎地处理信号量之间的处理顺序 , 否则很容易造成死锁现象 。
基于信号量给编程带来的隐患 , 于是有了提出了对开发者更加友好的并发编程模型-管程
管程Dijkstra 于 1971 年提出:把所有进程对某一种临界资源的同步操作都集中起来 , 构成一个所谓的秘书进程 。 凡要访问该临界资源的进程 , 都需先报告秘书 , 由秘书来实现诸进程对同一临界资源的互斥使用 , 这种机制就是管程 。
编程是一种在信号量机制上进行改进的并发编程模型 , 解决了信号量在临界区的 PV 操作上配对的麻烦 , 把配对的 PV 操作集中在一起而形成的并发编程方法理论 , 极大降低了使用和理解成本 。
管程由四部分组成:
  1. 管程内部的共享变量 。
  2. 管程内部的条件变量 。
  3. 管程内部并行执行的进程 。
  4. 对于局部与管程内部的共享数据设置初始值的语句 。
由此可见 , 管程就是一个对象监视器 。 任何线程想要访问该资源(共享变量) , 就要排队进入监控范围 。 进入之后 , 接受检查 , 不符合条件 , 则要继续等待 , 直到被通知 , 然后继续进入监视器 。
需要注意的事 , 信号量和管程两者是等价的 , 信号量可以实现管程 , 管程也可以实现信号量 , 只是两者的表现形式不同而已 , 管程对开发者更加友好 。
两者的区别如下
1.5w字,30图带你彻底掌握 AQS!(建议收藏)文章插图
管程为了解决信号量在临界区的 PV 操作上的配对的麻烦 , 把配对的 PV 操作集中在一起 , 并且加入了条件变量的概念 , 使得在多条件下线程间的同步实现变得更加简单 。
怎么理解管程中的入口等待队列 , 共享变量 , 条件变量等概念 , 有时候技术上的概念较难理解 , 我们可以借助生活中的场景来帮助我们理解 , 就以我们的就医场景为例来简单说明一下 , 正常的就医流程如下: