周一至周五早 8 点半! 精品技术文章准时送上!
一, 写在前面
上一篇文章聊了一下 java 并发中常用的原子类的原理和 Java 8 的优化, 具体请参见文章: 大白话聊聊 Java 并发面试问题之 Java 8 如何优化 CAS 性能?.
这篇文章, 我们来聊聊面试的时候比较有杀伤力的一个问题: 聊聊你对 AQS 的理解?
之前有同学反馈, 去互联网公司面试, 面试官聊到并发时就问到了这个问题. 当时那位同学内心估计受到了一万点伤害...
因为首先, 很多人还真的连 AQS 是什么都不知道, 可能听都没听说过. 或者有的人听说过 AQS 这个名词, 但是可能连具体全称怎么拼写都不知道.
更有甚者, 可能会说: AQS? 是不是一种思想? 我们平时开发怎么来用 AQS?
总体来说, 很多同学估计都对 AQS 有一种云里雾里的感觉, 如果用搜索引擎查一下 AQS 是什么? 看几篇文章, 估计就直接放弃了, 因为密密麻麻的文字, 实在是看不懂!
所以, 基于上述痛点, 咱们这篇文章, 就用最简单的大白话配合 N 多张手绘图, 给大家讲清楚 AQS 到底是什么? 让各位同学面试被问到这个问题时, 不至于不知所措.
二, ReentrantLock 和 AQS 的关系
首先我们来看看, 如果用 java 并发包下的 ReentrantLock 来加锁和释放锁, 是个什么样的感觉?
这个基本学过 java 的同学应该都会吧, 毕竟这个是 java 并发基本 API 的使用, 应该每个人都是学过的, 所以我们直接看一下代码就好了:
上面那段代码应该不难理解吧, 无非就是搞一个 Lock 对象, 然后加锁和释放锁.
你这时可能会问, 这个跟 AQS 有啥关系? 关系大了去了! 因为 java 并发包下很多 API 都是基于 AQS 来实现的加锁和释放锁等功能的, AQS 是 java 并发包的基础类.
举个例子, 比如说 ReentrantLock,ReentrantReadWriteLock 底层都是基于 AQS 来实现的.
那么 AQS 的全称是什么呢? AbstractQueuedSynchronizer, 抽象队列同步器. 给大家画一个图先, 看一下 ReentrantLock 和 AQS 之间的关系.
我们来看上面的图. 说白了, ReentrantLock 内部包含了一个 AQS 对象, 也就是 AbstractQueuedSynchronizer 类型的对象. 这个 AQS 对象就是 ReentrantLock 可以实现加锁和释放锁的关键性的核心组件.
三, ReentrantLock 加锁和释放锁的底层原理
好了, 那么现在如果有一个线程过来尝试用 ReentrantLock 的 lock() 方法进行加锁, 会发生什么事情呢?
很简单, 这个 AQS 对象内部有一个核心的变量叫做 state, 是 int 类型的, 代表了加锁的状态. 初始状态下, 这个 state 的值是 0.
另外, 这个 AQS 内部还有一个关键变量, 用来记录当前加锁的是哪个线程, 初始化状态下, 这个变量是 null.
接着线程 1 跑过来调用 ReentrantLock 的 lock() 方法尝试进行加锁, 这个加锁的过程, 直接就是用 CAS 操作将 state 值从 0 变为 1.
如果不知道 CAS 是啥的, 请看上篇文章, 大白话聊聊 Java 并发面试问题之 Java 8 如何优化 CAS 性能?
如果之前没人加过锁, 那么 state 的值肯定是 0, 此时线程 1 就可以加锁成功.
一旦线程 1 加锁成功了之后, 就可以设置当前加锁线程是自己. 所以大家看下面的图, 就是线程 1 跑过来加锁的一个过程.
其实看到这儿, 大家应该对所谓的 AQS 有感觉了. 说白了, 就是并发包里的一个核心组件, 里面有 state 变量, 加锁线程变量等核心的东西, 维护了加锁状态.
你会发现, ReentrantLock 这种东西只是一个外层的 API, 内核中的锁机制实现都是依赖 AQS 组件的.
这个 ReentrantLock 之所以用 Reentrant 打头, 意思就是他是一个可重入锁.
可重入锁的意思, 就是你可以对一个 ReentrantLock 对象多次执行 lock() 加锁和 unlock() 释放锁, 也就是可以对一个锁加多次, 叫做可重入加锁.
大家看明白了那个 state 变量之后, 就知道了如何进行可重入加锁!
其实每次线程 1 可重入加锁一次, 会判断一下当前加锁线程就是自己, 那么他自己就可以可重入多次加锁, 每次加锁就是把 state 的值给累加 1, 别的没啥变化.
接着, 如果线程 1 加锁了之后, 线程 2 跑过来加锁会怎么样呢?
我们来看看锁的互斥是如何实现的? 线程 2 跑过来一下看到, 哎呀! state 的值不是 0 啊? 所以 CAS 操作将 state 从 0 变为 1 的过程会失败, 因为 state 的值当前为 1, 说明已经有人加锁了!
接着线程 2 会看一下, 是不是自己之前加的锁啊? 当然不是了,"加锁线程" 这个变量明确记录了是线程 1 占用了这个锁, 所以线程 2 此时就是加锁失败.
给大家来一张图, 一起来感受一下这个过程:
接着, 线程 2 会将自己放入 AQS 中的一个等待队列, 因为自己尝试加锁失败了, 此时就要将自己放入队列中来等待, 等待线程 1 释放锁之后, 自己就可以重新尝试加锁了
所以大家可以看到, AQS 是如此的核心! AQS 内部还有一个等待队列, 专门放那些加锁失败的线程!
同样, 给大家来一张图, 一起感受一下:
接着, 线程 1 在执行完自己的业务逻辑代码之后, 就会释放锁! 他释放锁的过程非常的简单, 就是将 AQS 内的 state 变量的值递减 1, 如果 state 值为 0, 则彻底释放锁, 会将 "加锁线程" 变量也设置为 null!
整个过程, 参见下图:
接下来, 会从等待队列的队头唤醒线程 2 重新尝试加锁.
好! 线程 2 现在就重新尝试加锁, 这时还是用 CAS 操作将 state 从 0 变为 1, 此时就会成功, 成功之后代表加锁成功, 就会将 state 设置为 1.
此外, 还要把 "加锁线程" 设置为线程 2 自己, 同时线程 2 自己就从等待队列中出队了.
最后再来一张图, 大家来看看这个过程.
四, 总结
OK, 本文到这里为止, 基本借着 ReentrantLock 的加锁和释放锁的过程, 给大家讲清楚了其底层依赖的 AQS 的核心原理.
基本上大家把这篇文章看懂, 以后再也不会担心面试的时候被问到: 谈谈你对 AQS 的理解这种问题了.
其实一句话总结 AQS 就是一个并发包的基础组件, 用来实现各种锁, 各种同步组件的. 它包含了 state 变量, 加锁线程, 等待队列等并发中的核心组件.
来源: https://juejin.im/post/5c07e59cf265da617464a09c