在上一篇文章中, 凯哥对同步组件基础框架 - AbstractQueuedSynchronizer(AQS)做了大概的介绍. 我们知道 AQS 能够通过内置的 FIFO 队列来完成资源获取线程的排队工作. 那么 AQS 是怎么来维护这个排队工作的呢? 今天我们就来扒一扒 AQS 源码. 从源码中来看看是怎么维护对了的.
本篇是《凯哥 (凯哥 Java:kagejava) 并发编程学习》系列之《Lock 系列》教程的第一篇:《Java 并发包下锁学习第三篇 - 从源码学习 Java 并发是怎么维护内部线程队列的》.
在上篇我们知道 AQS 内部有个内部类 - Node 对象. 这个对象就是来维护线程对资源访问的排队工作的. 具体怎么操作的呢? 本文主要内容: Node 节点介绍; 在同步器中怎么为维护排队的流程图.
一: Node 节点对象介绍
在 AQS 内部有个 Node 对象的内部类. 我们来看看这个对象都有哪些属性:
简化后:
- static final class Node {
- // 线程等待状态
- volatile int waitStatus;
- // 当前节点的上一个节点
- volatile Node prev;
- // 当前节点对象
- volatile Node next;
- // 当前节点维护的线程对象
- volatile Thread thread;
- // 当前节点的下一个 (后续) 节点
- Node nextWaiter;
- }
对象中属性介绍
Int waitStatus:
对象里面有表示状态的 4 个属性:
static final int CANCELLED = 1: 线程从同步队列中取消
static final int SIGNAL = -1: 后续节点等待状态. 当前节点在获取到资源后, 在释放前需要断开和后续节点的连接. 在其释放后, 会通知后续节点, 使后续解决继续运行.
static final int CONDITION = -2: 当前节点等待中. 在等待 condition 通知. 也可以理解成在 condition 队列中.
static final int PROPAGATE = -3: 在共享模式下, 下一次无条件传播
0: 默认状态.
Node prev: 当前节点的上一个节点
Node Next: 当前解决的后续节点
Node nextWaiter: 可以理解为节点的类型. 是共享式还是独占式.
Thread thread: 当前获取到同步状态的线程对象.
具体可以如下图:
首先, 我们需要明白, 在数据结构中, 能够保持 FIFO 的结构是队列模式的. 但是队列有单项队列和循环队列两种. 那么, 同步器使用的是哪个队列方式呢?
从 Node 节点属性中, 我们可以看到前节点和后续节点的属性. 说明使用的是循环队列.
二: 维护线程排队的流程图
为了保证线程的安全, 同步器提供了几个 CAS 的方法. 如下图:
CAS 设置头节点, 设置下一个节点, 设置状态, 设置尾节点等.
操作流程可以简述如下图:
流程说明:
入队列
入队流程如下:
上图流程说明:
当多个线程同时来争夺资源的时候, 其中一个线程获取到了资源 (同步状态或者是锁), 这个时候获取到资源的线程就会被构造成头节点. 其他线无非获取到资源的线程会被构成成 Node 节点对象并被放到队列中. 被构造成 Node 节点的线程会排在队列尾部排队. 为了保证线程安全性, 同步器会基于 CAS 设置尾节点的方法(即: compareAndSetTail ()) 来保持线程安全性. 这个方法需要传递当前线程 "自己认为" 的尾节点和前一个节点, 当 CAS 执行成功之后, 当前节点才会正式与之前的节点建立关系. 被设置尾部的 Node 节点的 next 将指向头节点.
如上图中线程 3 会和线程 1 执行类似的操作, 把自己添加到队列的尾部. 这样就形成了一个完整的双向队列排队了.
出队列
出队流程图如下:
出队流程说明:
从入队流程图中我们可以看出, 所有争夺资源并发的线程都被排队了. 同步队列遵循 FIFO(先进先出). 所谓的首节点就是获取同步状态成功节点. 当来的首节点中的线程在释放同步状态的时候, 会断开自己与后续节点的关联关系, 然后会唤醒后续节点操作的. 当后续节点获取同步状态成功的时候, 就将自己设置为首节点, 原来的首节点就退出了队列. 如果原来的首节点还需要获取的话, 后将自己线程构造成 Node 节点对象, 然后进行排队.
来源: https://www.cnblogs.com/kaigejava/p/12612705.html