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


  1. 病人去挂号后 , 去候诊室等待叫号
  2. 叫到自己时 , 就可以进入就诊室就诊了
  3. 就诊时 , 有两种情况 , 一种是医生很快就确定病人的病 , 并作出诊断 , 诊断完成后 , 就通知下一位病人进来就诊 , 一种是医生无法确定病因 , 需要病人去做个验血 / CT 检查才能确定病情 , 于是病人就先去验个血 / CT
  4. 病人验完血 / 做完 CT 后 , 重新取号 , 等待叫号(进入入口等待队列)
  5. 病人等到自己的号,病人又重新拿着验血 / CT 报告去找医生就诊
整个流程如下
1.5w字,30图带你彻底掌握 AQS!(建议收藏)文章插图
那么管程是如何解决互斥和同步的呢
首先来看互斥 , 上文中医生即共享资源(也即共享变量) , 就诊室即为临界区 , 病人即线程 , 任何病人如果想要访问临界区 , 必须首先获取共享资源(即医生) , 入口一次只允许一个线程经过 , 在共享资源被占有的情况下 , 如果再有线程想占有共享资源 , 就需要到等待队列去等候 , 等到获取共享资源的线程释放资源后 , 等待队列中的线程就可以去竞争共享资源了 , 这样就解决了互斥问题 , 所以本质上管程是通过将共享资源及其对共享资源的操作(线程安全地获取和释放)封装起来来保证互斥性的 。
再来看同步 , 同步是通过文中的条件变量及其等待队列实现的 , 同步的实现分两种情况
  1. 病人进入就诊室后 , 无需做验血 / CT 等操作 , 于是医生诊断完成后 , 就会释放共享资源(解锁)去通知(notify , notifyAll)入口等待队列的下一个病人 , 下一个病人听到叫号后就能看医生了 。
  2. 如果病人进入就诊室后需要做验血 / CT 等操作 , 会去验血 / CT 队列(条件队列)排队 ,同时释放共享变量(医生) , 通知入口等待队列的其他病人(线程)去获取共享变量(医生) , 获得许可的线程执行完临界区的逻辑后会唤醒条件变量等待队列中的线程 , 将它放到入口等待队列中, 等到其获取共享变量(医生)时 , 即可进入入口(临界区)处理 。
在 Java 里 , 锁大多是依赖于管程来实现的 , 以大家熟悉的内置锁 synchronized 为例 , 它的实现原理如下 。
1.5w字,30图带你彻底掌握 AQS!(建议收藏)文章插图
可以看到 synchronized 锁也是基于管程实现的 , 只不过它只有且只有一个条件变量(就是锁对象本身)而已 , 这也是为什么JDK 要实现 Lock 说的原因之一 , Lock 支持多个条件变量 。
通过这样的类比 , 相信大家对管程的工作机制有了比较清晰的认识 , 为啥要花这么大的力气介绍管程呢 , 一来管程是解决并发问题的万能钥匙 , 二来 AQS 是基于 Java 并发表中管程的一种实现 , 所以理解管程对我们理解 AQS 会大有帮助 , 接下来我们就来看看 AQS 是如何工作的 。
AQS 实现原理AQS 全称是 AbstractQueuedSynchronizer , 是一个用来构建锁和同步器的框架 , 它维护了一个共享资源 state 和一个 FIFO 的等待队列(即上文中管程的入口等待队列) , 底层利用了 CAS 机制来保证操作的原则性 。
AQS 实现锁的主要原理如下:
1.5w字,30图带你彻底掌握 AQS!(建议收藏)文章插图
以实现独占锁为例(即当前资源只能被一个线程占有) , 其实现原理如下:state 初始化 0 , 在多线程条件下 , 线程要执行临界区的代码 , 必须首先获取 state , 某个线程获取成功之后 ,state 加 1 , 其他线程再获取的话由于共享资源已被占用 , 所以会到 FIFO 等待队列去等待 , 等占有 state 的线程执行完临界区的代码释放资源( state 减 1)后 , 会唤醒 FIFO 中的下一个等待线程(head 中的下一个结点)去获取 state 。