AQS的CLH同步队列
同步队列是双向。
与CLH同步锁异同
CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形, 其主要从两方面进行了改造:节点的结构与节点等待机制。
1.结构上引入了头结点和尾节点。
他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,并且每个节点都引入前驱节点和后后续节点的引用。
2.等待机制上由原来的自旋改成阻塞唤醒。
知道其结构了,我们再看看他的实现。在线程获取锁时会调用AQS的acquire()方法,该方法第一次尝试获取锁如果失败,会将该线程加入到CLH队列中:
public final void acquire(int arg) {
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;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
CLH队列节点Node源码
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/***
NEW(0):新结点入队时的默认状态
CANCELLED(1):表示当前结点已取消调度。
当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
SIGNAL(-1):
表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL即-1。
CONDITION(-2):
表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后
CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE(-3):
共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
***/
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;
volatile Thread thread;
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
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;
}
}
入队源码
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//第一轮:如果队列中还没有元素 tail 为 null
if (t == null) {
// 第一轮:使用cas 将 head 从 null 变为 new Node()
// 假设新创建的这个空节点A
// head节点的属性:thread=null
if (
compareAndSetHead(new Node()))
//第一轮:将head赋值为tail
//第一轮:此时head和tail是同一个节点 是新创建的节点A
//第一轮:进入第二轮循环
tail = head;
} else {
// 第二轮:将原来的尾结点tail即A赋值给新的node即B的prev
// 即新节点B的prev指向头结点A
node.prev = t;
// 第二轮:将新节点B作为尾节点
if (compareAndSetTail(t, node)) {
// 第二轮:将头结点A的nex指向B
t.next = node;
return t;
}
}
}
}
//第一次循环
//head = yummy节点
//tail = yummy节点
//第二次循环
//新节点的前置节点指向head
//尾节点被设置为新节点
//头结点的后置节点指向新节点
//此时
//head ← 新节点
//tail = 新节点
//head → 新节点
//因为尾节点就是新节点
//所以就是
//head→tail(新节点)
//head←tail(新节点)
head是yummy节点,head后面的节点都是正常节点。
入队代码测试
package AQS;
import static AQS.AqsNode.Node.enter;
public class AqsNode {
@Override
public String toString() {
return "AqsNode{" +
"head=" + head.name +
", tail=" + tail.name +
", state=" + state +
'}';
}
private static transient volatile Node head;
private static transient volatile Node tail;
private volatile int state;
private static Node enq(final Node node) {
for (; ; ) {
System.out.println(head);
Node t = tail;
if (t == null) {
Node node1 = new Node();
node1.name = "YUMMY";
if (compareAndSetHead(node1))
tail = head;
} else {
//关键点:这里要把t当做头结点来看
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
System.out.println(head);
}
}
private static boolean compareAndSetTail(Node t, Node node) {
tail = node;
return true;
}
private static boolean compareAndSetHead(Node node) {
head = node;
return true;
}
static final class Node {
public String name;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
@Override
public String toString() {
return "Node{" +
"name='" + name + ''' +
'}';
}
public static void enter() {
AqsNode.Node node1 = new AqsNode.Node();
node1.name = "node1";
AqsNode.Node node2 = new AqsNode.Node();
node2.name = "node2";
enq(node1);
enq(node2);
}
}
public static void main(String[] args) {
enter();
}
}
入队代码图解
第一个节点入队前:头节点和尾节点没有连接,且头尾节点的前置节点和后置节点为空。
第一个节点入队后:头节点yummy的后置节点指向尾节点node1,尾节点的前置节点指向头结点yummy。头结点的前置节点和尾节点的后置节点为空。
第二个节点入队后:头结点的前置节点和尾节点的后置节点为空。
出队代码
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//线程2从死循环中醒来 发现自己的前置节点是头结点 获取锁成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
假设t1加锁成功 t2在同步队列中。
t1释放锁的时候,t2会被unpark,然后执行死循环发现t2的前置节点是头结点,然后t2获取锁成功,进入判断:
/***
执行出队操作。
为什么说是执行出队操作呢?
因为本来节点是 Head → t2
现在把Head移除了 t2作为新的Head
***/
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//将当前节点设置为头结点
setHead(node);
//将原来头结点的next指针指向空
p.next = null;//help GC
failed = false;
// 还是需要获得锁后, 才能返回打断状态
return interrupted;
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
Node状态:waitStatus
Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。
变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
-
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态。
-
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL即-1。
-
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
-
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
-
0:新结点入队时的默认状态。
新建节点一般都为0,负值表示结点处于有效等待状态,而正值表示结点已被取消。
所以源码中很多地方用>0、<0来判断结点的状态是否正常,0是新结点入队时的默认状态。
已取消
线程因为超时或者中断涉及到取消的操作,如果某个节点被取消了,那个该节点就不会参与到锁竞争当中,它会等待GC回收。
取消的主要过程是将取消状态的节点移除掉,移除的过程还是比较简单的。先将其状态设置为CANCELLED,然后将其前驱节点指向其后继节点。
假设当前队列结构如下:A------>B----->C
现在要取消B节点 即将B的前置节点A指向B的后置节点C
A------>C
当然这个过程仍然会是一个CAS操作:
node.waitStatus = Node.CANCELLED;
Node pred = node.prev;
Node predNext = pred.next;
Node next = node.next;
Node指针
volatile Node prev;//同步队列中节点的前置节点
volatile Node next;//同步队列中节点的后置节点
Node nextWaiter;//等待队列中节点的后置节点
同步队列是双向,条件等待队列是单向。