一 简介
AbstractQueuedSynchronizer,即队列同步器(简称AQS)。它是构建锁或者其他同步组件的基础框架。它的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
二 AQS的框架说明
AbstractQueuedSynchronizer继承自AbstractOwnableSynchronizer。AbstractOwnableSynchronizer有一个成员变量,这个变量代表的是独占模式同步的当前所有者。
AbstractQueuedSynchronizer内部实现了独占模式和共享模式。其实现原理就是通过内部维护一个被volatile所以修饰的int变量和一个FIFO的队列,队列中的节点去获取int变量,一旦获取到那么意味着该节点(AQS中的内部类中的Node一会源码中会说明)中封装的线程就会持有执行时间。下面是网上找到的图。
从上图中我们不难看出同步队列与资源的关系,上面说明了同步队列和资源的关系,下面我们在看一下,等待队列与同步队列的关系。
当调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。
如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
三 源码分析
在AQS中的同步队列和等待队列中都用到了一个共同一个内部的Node类。根据图一可以猜测在Node类中一定存在的有Thread、前驱结点(prev)的引用和后驱节点(next)的引用。上面这三项就构成可同步队列最基础的东西。根据图二我们猜测Node中一定存在一个用来区分是等待这个节点是等待状态还是同步状态还是取消状态,又一个变量出来了waitStatus,在看等待队列的话那么也就需要一个指向下一个节点的引用(nextWaiter)。至此已经推测出Node中自少也有五个变量。分别是Thread、 prev、 next、 waitStatus、nextWaiter。下面是源码来验证猜测是否正确。
static final class Node {
/**
* 表明这个节点是一个共享模式
*/
static final Node SHARED = new Node();
/**
* 表明这个节点是一个独占模式
*/
static final Node EXCLUSIVE = null;
/**
* 由于在同步对列中等待的线程等待超时或者被中断, 需要从同步对列中取消等待,节点进入该状态将不会变化
*/
static final int CANCELLED = 1;
/**
* 后继节点的线程处于等待状态,当前面节点的线程如果释放了同步状态或取消, 将会通知后继节点,使后继级节点的线程可以运行.
*/
static final int SIGNAL = -1;
/**
* 节点在等待对列中,节点线程等待在Condition上, 当其他线程对Condition调用了 signal()方法后, 该节点将会从等待对列中转移到同步对列中,加入到同步状态的获取
*/
static final int CONDITION = -2;
/**
* 表示下一次共享模式同步状态获取将会无条件的被传播下去,已经释放了
*/
static final int PROPAGATE = -3;
/**
* 节点的状态
*/
volatile int waitStatus;
/**
* 前驱节点
*/
volatile Node prev;
/**
* 后驱节点
*/
volatile Node next;
/**
* node对应的线程
*/
volatile Thread thread;
/**
* 等待对列中的后继节点.如果当前节点是共享的那么这个字段将是一个SHARED常量, 也是就是说节点类型和等待对列中的后继节点共用同一个字段.
*/
Node nextWaiter;
/**
* 是不是共享
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前驱节点
*
* @return node 对象
*/
final Node predecessor() {
Node p = prev;
if (p == null) {
throw new NullPointerException();
}
return p;
}
Node() {
}
Node(Thread thread, Node node) {
this.thread = thread;
this.nextWaiter = node;
}
Node(Thread thread, int waitStatus) {
this.thread = thread;
this.waitStatus = waitStatus;
}
}
看源码有几个已经猜对了,其中多出来几个int类型的值(SIGNAl等)和Node类型(SHARED),多出来的int类型的值就是waitSatus可以选择的值,多出来的Node用来表示是独占模式和共享模式。
现在已经知道了同步队列的节点是怎么构成的了,接下来的问题就是如何操作这些节点来构成一个同步队列,又是怎么样去操作线程的呢。
下面看一段源码这段源码,这段代码的意思是向队列中添加一个节点,具体的执行流程已经注释写的很清楚了。
/**
* 队列的头结点
*/
private transient volatile Node head;
/**
* 队列的尾节点
*/
private transient volatile Node tail;
/**
* 添加一个同步节点
*
* @param mode 节点
* @return 节点返回
*/
private Node addWaiter(Node mode) {
//创建一个节点这个节点将当前线程封装了
Node node = new Node(Thread.currentThread(), mode);
//获取队列的尾部节点
Node pred = tail;
//当尾节点不为空是先尝试替换一下,成功的话直接返回
if (pred != null) {
node.prev = pred;
//这里要注意了compareAndSetTail是CAS操作
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//循环替换
enq(node);
return node;
}
看完上面的方法可能会有一些问题,没看到队列的初始化就直接添加了吗?在上面源码中调用了一个方法循环替换。下面是源码
/**
* 添加一个节点到尾部
*
* @param node 节点
* @return 返回这个节点的前驱节点
*/
private Node enq(Node node) {
for (; ; ) {
//获取当前的尾部节点
Node t = tail;
if (t == null) {
//在这里进行初始化这样的话头节点和尾节点都不为null,当下一次
//循环的时候tail循环的时候tail就不为null,这样就可以将传递的参数
//设置为尾节点了
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
//尾部节点不为null这时就要添加了
//将传入的节点的前驱节点设置为当前的尾节点当第一次初始化的时候
//指向了头节点不是第一次的话将前驱结点设置为尾部节点
node.prev = t;
//使用CAS设置对列的尾节点 这一步如果失败的话会继续for循环
//直到成功替换
if (compareAndSetTail(t, node)) {
//当替换成功尾部节点的时候将尾部节点的后驱节点设置为添加的节点
t.next = node;
return t;
}
}
}
}
这个方法中我们可以看到首先判断了一些队列的尾部节点是否为空,如果尾节点为空的话,那么队列就可能为空,使用CAS设置将头节点初始化,同时也将尾部节点初始化。这样的话这个队列也就被初始化了,接下来就是设置尾部节点,和设置该节点的前驱和后驱节点了。
同步队列的初始化添加和维护就已经结束了接下来就是资源的竞争了。关于资源获取就有两种猜想了
猜想一:线程上来就直接获取,如果获取成功的话那就执行了,获取失败的话被封装成节点添加到同步队列的尾部
猜想二:线程一上来看看队列中有没有要同步的节点,如果有的话那就不获取资源了直接添加到同步队列中,等待上一个节点唤醒。
四 总结
1.AQS的整体架构:基于模板方法的设计模式,主要构成有state,同步对列和等待对列
2.AQS的同步队列的添加节点,与初始化。其中的主要方法是addWaiter()
3.资源获取的猜想,下一篇总结具体的队列的节点是如果获取资源的。