历史
1995 年 sun 公司发布了第一个 java 语言版本, 可以说从 jdk1.1 到 jdk1.4 期间 java 的使用主要是在移动应用和中小型企业应用中, 在此类领域中基本不用设计大型并发场景, 当然也没有大型互联网公司使用 java, 因为担心它本身的性能.
在互联网及服务器硬件迅猛的发展下, sun 公司更加注重企业级应用方面, 毫无疑问高并发是一个重要的主题, 于是在 J2SE5.0(jdk1.5)代号为老虎的版本中增加了更加强大的并发相关的操作包 --java.util.concurrent.
此后 java 在高并发中表现优异, 很多大型互联网公司都使用 java 作为主要开发语言, 例如阿里巴巴, ebay 等, 这些公司系统的访问绝对是属于世界级的大型并发场景, 反映了 java 在大型并发场景是可行的.
AQS 框架
Jdk 的并发包提供了各种锁及同步机制, 其实现的核心类是 AbstractQueuedSynchronizer, 我们简称为 AQS 框架, 它为不同场景提供了实现锁及同步机制的基本框架, 为同步状态的原子性管理, 线程的阻塞, 线程的解除阻塞及排队管理提供了一种通用的机制.
Jdk 的并发包 (juc) 的作者是 Doug Lea, 但其中思想却是结合了多位大师的智慧, 如果你想深入理解 juc 的相关理论可以参考 Doug Lea 写的《The_java.util.concurrent_Synchronizer_Framework》论文. 从这里可以找到 AQS 的理论基础, 包括框架的基本原理, 需求, 设计, 实现思路, 用法及性能, 由于这些方面篇幅较大, 本文不打算涉及所有方面, 主要将针对 AQS 类的结构及相关操作进行分析.
AQS 队列
AQS 将线程封装到一个 Node 里面, 并维护一个 CHL Node FIFO 队列, 它是一个非阻塞的 FIFO 队列, 也就是说在并发条件下往此队列做插入或移除操作不会阻塞, 是通过自旋锁和 CAS 保证节点插入和移除的原子性, 实现无锁快速插入.
其实 AbstractQueuedSynchronizer 主要就是维护了一个 state 属性, 一个 FIFO 队列和线程的阻塞与解除阻塞操作. state 表示同步状态, 它的类型为 32 位整型, 对 state 的更新必须要保证原子性. 这里的队列是一个双向链表, 每个节点里面都有一个 prev 和 next, 它们分别是前一个节点和后一个节点的引用. 需要注意的是此双向链表除了链头其他每个节点内部都包含一个线程, 而链头可以理解为一个空节点.
队列结构
对于队列的结构我们需要深入理解下, 下图展示的是组成双向链表其中一个节点的结构, 该节点包含五个主要元素, 表示的意思如下表,
Node prev: 前驱节点, 指向前一个节点
Node next: 后续节点, 指向后一个节点
Node nextWaiter: 用于存储 condition 队列的后续节点
Thread thread: 入队列时的当前线程
int waitStatus: 有五种状态:
SIGNAL, 值为 - 1, 表示当前节点的后续节点中的线程通过 park 被阻塞了, 当前节点在释放或取消时要通过 unpark 解除它的阻塞.
CANCELLED, 值为 1, 表示当前节点的线程因为超时或中断被取消了.
CONDITION, 值为 - 2, 表示当前节点在 condition 队列中.
PROPAGATE, 值为 - 3, 共享模式的头结点可能处于此状态, 表示无条件往下传播, 引入此状态是为了优化锁竞争, 使队列中线程有序地一个一个唤醒.
0, 除了以上四种状态的第五种状态, 一般是节点初始状态.
前驱节点 prev 的引入主要是为了完成超时及取消语义, 前驱节点取消后只需向前找到一个未取消的前驱节点即可; 后续节点的引入主要是为了优化后续节点的查找, 避免每次从尾部向前查找; nextWaiter 用于表示 condition 队列的后续节点, 此时 prev 和 next 属性将不再使用, 而且节点状态处于 Node.CONDITION; waitStatus 表示的是后续节点状态, 这是因为 AQS 中使用 CLH 队列实现线程的结构管理, 而 CLH 结构正是用前一节点某一属性表示当前节点的状态, 这样更容易实现取消和超时功能.
总结
上面是对节点及节点组成队列的结构的介绍, 后面户口介绍 AQS 相关的一些操作, 包括锁的获取与释放, 队列的管理, 同步状态的更新, 线程阻塞与唤醒, 取消中断与超时中断等等.
来源: https://juejin.im/post/5bb018e9e51d450e8c3508b4