Java - AQS(一)
在Java中,AQS代表AbstractQueuedSynchronizer(抽象队列同步器)。AQS是Java并发包中用于构建同步器的基础框架。它提供了一种实现同步状态管理、线程等待和通知的机制。
AQS主要通过一个int类型的状态值来表示同步状态,并提供了一些方法来操作和管理这个状态。它使用一个FIFO(先进先出)的等待队列来管理等待线程,并通过内部的一些算法和机制来确保线程的安全等待和唤醒。
AQS的核心思想是通过继承和重写来实现具体的同步器。它提供了几个关键的方法供子类实现,包括getState()
(获取当前状态值)、setState()
(设置状态值)、acquire()
(获取同步状态)、release()
(释放同步状态)等等。子类可以根据自己的需求实现这些方法来创建不同类型的同步器,例如ReentrantLock、Semaphore等。
在java.util.concurrent.locks
包下,有两个这样的类:
- AbstractQueuedSynchronizer
- AbstractQueuedLongSynchronizer
这两个类的唯一区别就是:
- AbstractQueuedSynchronizer内部维护的
state
变量是int
类型 - AbstractQueuedLongSynchronizer内部维护的
state
变量是long
类型
我们常说的AQS
其实泛指的就是这两个类,即抽象队列同步器
。
AQS其实主要做了这么几件事情:
- 同步状态(state)的维护管理
- 等待队列的维护管理
- 线程的阻塞与唤醒
可以通过类比来理解AQS的原理。
想象一下,你在一家餐厅等待就餐。餐厅只有一张餐桌,同时只能容纳一组客人就座。当你到达餐厅时,你会查看餐桌的状态,如果餐桌已经被占用,你就需要在等待区等待。一旦餐桌被释放,下一位等待的客人将获得就餐权,并将其状态设置为“占用”。
这里,餐桌就可以类比为AQS中的同步状态,每个客人就是一个线程。餐厅的等待区则对应AQS中的等待队列。通过这种方式,AQS提供了一种线程同步的机制。
AQS内部维护了一个状态变量,可以通过getState()
方法获取该状态。线程可以通过acquire()
方法来获取同步状态,如果当前状态已被其他线程占用,那么该线程将被放入AQS的等待队列中,进入等待状态。当其他线程释放同步状态时,AQS会从等待队列中选择一个线程,将状态授予它,使其继续执行。这个过程就类似于等待餐桌的释放和下一位客人就座。
AQS还提供了一些方法供子类实现,如tryAcquire()
和tryRelease()
等。子类可以通过重写这些方法来定义特定的获取和释放同步状态的逻辑,以实现不同类型的同步器。
总之,AQS提供了一个通用的框架,通过维护同步状态和等待队列来实现线程的同步和互斥。类比于餐厅中的餐桌和等待区,可以帮助理解AQS的基本原理。
内部类
Node
在AbstractQueuedSynchronizer(AQS)类中,Node类是用于构建等待队列的节点。每个线程在等待获取同步状态时,都会被封装成一个Node节点并加入到AQS的等待队列中。 看过LinkedList源码,可以发现这里类似于内部类中的Node,都有头尾指针,只不过加入了私有变量waitStatus状态,同时用 volatile Thread存放内存可见性线程
/**
* 源码翻译
*
*等待队列节点类。
等待队列是“CLH”(Craig、Landin和Hagersten)锁定队列的一种变体。CLH锁通常用于自旋锁。相反,我们将它们用于阻塞同步器,但使用相同的基本策略,即在其节点的前身中保存有关线程的一些控制信息。每个节点中的“状态”字段跟踪线程是否应该阻塞。当一个节点的前身释放时,它会收到信号。队列的每个节点充当特定通知样式的监视器,持有单个等待线程。状态字段并不控制线程是否被授予锁等。如果线程位于队列的第一个,则可以尝试获取。但做第一并不能保证成功;它只赋予人们争辩的权利。因此,当前释放的竞争者线程可能需要重新等待。
要排队进入CLH锁,需要自动地将其拼接为新尾。要脱队,只需设置head字段。
+------+ prev +-----+ +-----+
头部| | <---- | | <---- |尾部
+------+ +-----+ +-----+
插入到CLH队列只需要对“尾部”进行单个原子操作,因此存在从未排队到排队的简单原子分界点。类似地,脱离队列只涉及更新“头部”。然而,节点需要做更多的工作来确定谁是它们的后继者,部分原因是为了处理由于超时和中断而可能取消的情况。
“prev”链接(在原来的CLH锁中没有使用)主要用于处理取消。如果一个节点被取消,它的后继节点(通常)被重新链接到一个未取消的前继节点。有关自旋锁的类似力学解释,请参阅Scott和Scherer在http://www.cs.rochester.edu/u/scott/synchronization/上发表的论文
我们还使用“next”链接来执行阻塞机制。每个节点的线程id都保存在自己的节点中,因此前导节点通过遍历下一个链接来确定它是哪个线程,从而通知要唤醒的下一个节点。后继者的确定必须避免与新排队的节点竞争,以设置其前继者的“next”字段。当节点的后继节点为空时,可以通过从自动更新的“尾部”向后检查来解决这个问题。(或者换句话说,下一个链接是一种优化,因此我们通常不需要向后扫描。)
消去为基本算法引入了一定的保守性。由于我们必须轮询其他节点的取消,因此我们可能无法注意到被取消的节点是在我们前面还是后面。解决这个问题的办法是,在继任者被取消时,总是取消他们的停车位,让他们在一个新的前任上稳定下来,除非我们能找到一个未被取消的前任来承担这项责任。
CLH队列需要一个虚拟头节点才能启动。但我们不会在构建时创建它们,因为如果没有争用,那将是浪费精力。相反,将构造节点,并在第一次争用时设置头和尾指针。
等待条件的线程使用相同的节点,但使用额外的链接。条件只需要在简单(非并发)链接队列中链接节点,因为它们只有在独占状态下才会被访问。等待时,将节点插入条件队列。收到信号后,节点被转移到主队列。状态字段的特殊值用于标记节点所在的队列。
/**
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
Node类包含了多个字段,其中比较重要的字段有:
prev
:指向前一个节点的引用,形成了一个双向链表结构,用于在等待队列中维护节点的顺序。next
:指向后一个节点的引用,同样用于维护节点的顺序。thread
:表示持有该节点的线程。waitStatus
:表示节点的等待状态,具体的取值有以下几种:CANCELLED
:表示节点已被取消。SIGNAL
:表示节点需要唤醒后继节点。CONDITION
:表示节点处于等待队列中,等待在条件变量上。PROPAGATE
:表示释放共享锁时需要进行传播。0
:表示节点处于初始状态。
nextWaiter
:指向下一个等待在同一个条件变量上的节点。
通过使用Node类,AQS可以构建一个基于双向链表的等待队列,用于管理等待获取同步状态的线程。等待队列中的每个节点都代表一个等待线程,并包含了线程的相关信息和状态。AQS可以通过操作等待队列中的节点来实现线程的等待和唤醒机制,以及实现不同类型的同步器的特定语义。
Node类在AQS内部使用,并不需要直接在使用AQS的代码中引用或操作该类。它是AQS内部数据结构的一部分,用于支持等待队列的构建和管理。
ConditionObject
官方文档:
作为Lock实现基础的AbstractQueuedSynchronizer的条件实现。
该类的方法文档从锁定和条件用户的角度描述了机制,而不是行为规范。这个类的导出版本通常需要附带描述依赖于相关AbstractQueuedSynchronizer的条件语义的文档。
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
ConditionObject类(状态对象)是AbstractQueuedSynchronizer(AQS)内部的一个内部类,看起来有点抽象,简而言之,它实现了Condition接口,用于支持基于条件的线程等待和唤醒机制。
在多线程编程中,有时需要线程在某个条件满足时等待,而在条件不满足时进行阻塞,直到其他线程发出相应的信号。ConditionObject提供了这样的功能,它可以和AQS一起使用,使得同步器能够支持条件等待和唤醒操作。
ConditionObject提供了几个关键的方法,包括:
await()
:使当前线程等待,并释放持有的同步状态,直到被其他线程发出的信号唤醒。awaitUninterruptibly()
:与await()
类似,但是在等待过程中不响应中断信号。signal()
:唤醒等待在该条件上的一个线程,使其继续执行。signalAll()
:唤醒等待在该条件上的所有线程,使它们继续执行。
小结
上方有Node类下方ConditionObject中又存有firstWaiter对象,他们之间有什么关联?
在AQS的等待队列中,每个节点都是一个Node
对象,用于表示一个等待条件的线程。这些节点通过prev
和next
字段组成双向链表结构,形成了等待队列。
当使用Condition
对象进行等待操作时,会将当前线程封装成一个Node
节点,并将其加入到等待队列的尾部,通过lastWaiter
字段来指向新添加的节点。当条件满足并需要唤醒等待线程时,可以从等待队列的头部获取第一个等待线程的Node
节点,通过firstWaiter
字段来引用它。
通过firstWaiter
和lastWaiter
字段,ConditionObject
可以有效地维护和操作等待队列,实现条件等待和唤醒的功能。这与AQS的等待队列和Node
节点密切相关,用于支持基于条件的线程等待和唤醒机制。