1.什么是队列同步器
用来构建锁或者其他同步组件的基础框架,使用int型的成员变量来表示同步的状态,线程以及等待状态等信息被封装成了Node节点,而这些Node节点采用先进先出的队列来进行排队管理。
2.队列同步器所属包
package java.util.concurrent.locks;
3.队列同步器继承与实现关系
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable
4.同步队列Node的数据结构
双向队列-数据结构图:
解释:对于这个链式队列结构,没有成功获取同步状态的线程将会成为节点加入到队列的尾部,而队列中的节点获取锁资源是从头部开始的
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;
/**
* 获取同步状态的线程
*/
volatile Thread thread;
/**
* 等待队列中的后继结点。
* 如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是节点类型(独占和共享)和等待队列中的后继结点共用同一个字段
*/
Node nextWaiter;
/**
* 等待的节点是否共享
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 获取前驱节点,但需要判断下前驱节点是否为空,否则将会抛出空指针异常
* @return the predecessor of this node
*/
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;
}
}
5.常用属性
/**
* 等待队列的头结点,采用懒初始化的方式(意思就是只有在需要头结点的时候才进行初始化)。
* 如果头结点存在,那么waitStatus状态就不会被标记位CANCELLED
*/
private transient volatile Node head;
/**
* 等待队列的尾节点,采用懒初始化的方式。
* 使用enq方法进行添加新的节点或修改节点。
*/
private transient volatile Node tail;
/**
* 同步状态
*/
private volatile int state;
6.常用方法
getState()方法:
/**
* 获取同步状态的当前值。操作具有volatile读的语义。
*/
protected final int getState() {
return state;
}
setState()
方法:
/**
* 设置同步状态的值。操作具有volatile写的语义。
*/
protected final void setState(int newState) {
state = newState;
}
compareAndSetState()
方法:
/**
* 如果当前状态值等于预期值,那么将以原子的方式将当前同步状态的值更新给定值。这个操作具有volatile读写语义。
* 原子性指要么更新过程成功,要么过程中断,还是原样。
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
解释:这个方法采用cas原则来保证状态设置的原子性,cas本身采用乐观锁的方式,从而不会产生线程的阻塞问题。由于采用volatile读写语义,那么线程访问是保持一致性的。理由:因为volatile实现原则是将缓存中的数据写入到主存中的。所以每个线程读写的数据都是从主存中获取来的,而不是每个线程缓存的数据,所以保证了一致性。
7.独占式同步状态的获取
tryAcquire()方法:查询是否允许它在独占模式下获取对象状态,如果允许则获取它。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
acquire(int arg) 方法:以独占模式获取对象,忽略中断
/**
* 以独占模式获取对象,忽略中断。
*/
public final void acquire(int arg) {
//如果没获取到队列的状态并且不停循环的方式获取线程的结果中断失败标志为true
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//中断线程
selfInterrupt();
}
addWaiter方法:为当前线程和给定的模式创建节点
/**
* 为当前的线程和给定的模式创建节点
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//获取尾节点
Node pred = tail;
//如果尾节点不为空
if (pred != null) {
//设置创建节点的前驱为尾节点
node.prev = pred;
//判断当前状态值是否等于预期的尾节点pred,如果等于就将当前同步状态值更新尾节点为node
if (compareAndSetTail(pred, node)) {
//尾节点的后继结点为node
pred.next = node;
return node;
}
}
//将节点插入到队列
enq(node);
return node;
}
注意:方法compareAndSetTail采用原子的方式进行更新尾节点。
enq方法:将节点插入到队列中
/**
* 将节点插入到队列
*/
private Node enq(final Node node) {
for (;;) {
//获取尾节点为t
Node t = tail;
//如果尾节点为空
if (t == null) {
/*
* 初始化一个头结点作为尾节点t
* 如果节点node第一次入队列,会创建新的节点new node()作为头节点
* 而此时队列的尾节点和头结点是同一个节点
*/
if (compareAndSetHead(new Node()))
tail = head;
} else {
//设置节点node的前驱为t
node.prev = t;
//判断node节点是否是预期的尾节点t,如果是就更新尾节点t为node
if (compareAndSetTail(t, node)) {
//尾节点的后继结点为node
t.next = node;
return t;
}
}
}
}
acquireQueued方法:以死循环的方式不停的获取同步状态,如果获取到返回true,否则false
/**
* 以死循环的方式不停的获取同步状态
* 如果获取不到则阻塞节点中的线程,
* 而被阻塞线程只能依靠其前驱节点的出队列操作或者阻塞线程中断
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取节点node的前驱节点p
final Node p = node.predecessor();
/* 如果p为头节点,并且已经成功获取了同步对象
* 获取成功就将队列头结点取出,
* 将头结点的后继结点作为头结点,
* 删除头结点与后继结点的关系
*/
if (p == head && tryAcquire(arg)) {
//设置头节点为node
setHead(node);
//前驱节点p(头结点)的后继结点设置为null
p.next = null; // help GC
//失败标志设置为false
failed = false;
//返回中断失败标志false
return interrupted;
}
//检查并更新无法获取的节点的状态并且检查是否中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//设置中断失败标志为true
interrupted = true;
}
} finally {
//如果中断标志位为true
if (failed)
//取消正在尝试执行的请求
cancelAcquire(node);
}
}
问题:为什么死循环中,只有前驱节点为头结点才能够尝试获取同步状态?
首先清楚的是头节点是成功获取同步状态的节点。当头节点的线程释放了同步状态之后,将唤醒其后继节点,后继节点唤醒时也得检查其前驱节点是否为头结点。(而先进先出队列本身就是这种前驱后继关系,不可能当前节点的前驱节点没有被唤醒获取同步状态,就直接跳到当前节点。其次如果当前节点的前驱节点不是头结点这个限制条件,那么当前节点的前驱节点还有其前驱节点(就是前前驱节点没有被唤醒获取同步状态),还是不满足从队列头开始获取同步状态)。
acquire方法的流程图:
8.共享式同步状态的获取
tryAcquireShared() 方法:查询是否允许在共享模式下获取对象的状态,如果允许,那么就获取它
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
acquireShared() 方法:共享模式获取对象,忽略中断
public final void acquireShared(int arg) {
//是否允许在共享模式下获取对象
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
doAcquireShared() 方法:在共享模式下,以死循环的方式不停地获取同步状态
private void doAcquireShared(int arg) {
//获取共享模式下创建的节点
final Node node = addWaiter(Node.SHARED);
//失败标志位默认为true
boolean failed = true;
try {
//中断标志位默认为false
boolean interrupted = false;
for (;;) {
//获取节点的前驱节点p
final Node p = node.predecessor();
//如果p为头结点
if (p == head) {
//是否允许在共享模式下获取对象
int r = tryAcquireShared(arg);
if (r >= 0) {
//设置队列的头结点
setHeadAndPropagate(node, r);
p.next = null; // help GC
//如果中断标志位为true,则中断当前线程
if (interrupted)
selfInterrupt();
//设置失败标志位为false
failed = false;
return;
}
}
//检查并更新无法获取的节点的状态并且检查是否中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//设置中断失败标志为true
interrupted = true;
}
} finally {
//如果中断标志位为true
if (failed)
//取消正在尝试执行的请求
cancelAcquire(node);
}
}
解释:在共享模式获取自旋过程中,成功获取同步状态并退出自旋的条件是tryAcquireShared() 方法返回值大于等于0。如果当前节点的前驱节点为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
9.阅读总结
(1)AQS是采用双向链队列这种数据结构实现的。
(2)AQS没有获取同步状态的线程节点都加入到队列的尾部,AQS是从头节点开始获取锁资源。
(3)AQS获取同步状态采用死循环+cas来保证