介绍
说到java中的锁就会想到synchronized和lock,而说到lock一般就会想到ReentrantLock,而ReentrantLock的底层实现就是依赖于AbstractQueuedSynchronizer,也就是AQS。
AQS已经实现了底层的逻辑,对于想要自己实现锁,只需要继承AQS然后维护其state状态就可以了。
源码解析
状态解析
waitStatus状态解析:
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
0:新结点入队时的默认状态。
获取锁
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁,实现由自己实现
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 新建一个Node,加入到等待队列
selfInterrupt(); // 自我打断
}
tryAcquire主要是尝试获取锁,在AQS中并没有实现,只是抛出了异常,这个是留给具体实现锁的类去做的,比如ReentrantLock等。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速加入queue,如果失败或者是还没初始化tail则使用enq方法
Node pred = tail;
if (pred != null) {
// 将node的前一个节点设置成原来的tail
node.prev = pred;
// cas将此Node设置为tail
if (compareAndSetTail(pred, node)) {
// 将原来tail的下一个节点设置成此node
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
// 自旋
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 初始化head和tail,此时tail和head一致,继续自旋走else逻辑
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 上面已经把head和tail初始化完成
// 将此节点的前一个节点设置成tail
node.prev = t;
// cas将此节点设置成新的tail
if (compareAndSetTail(t, node)) {
// 将原来的tail节点的下一个节点指向新的tail节点
t.next = node;
return t;
}
}
}
}
这样就完成了新的Node加入到尾部的逻辑中了,但是仅仅这样还是不够。因为从此时,我们并没有看到线程的中断,那么它并没有阻塞。还有就是它中断之后该如何处理,如何唤醒等等的问题还没有处理,这些问题都在acquireQueued的方法中。
final boolean acquireQueued(final Node node, int arg) {
// 失败标识
boolean failed = true;
try {
// 线程是否打断过
boolean interrupted = false;
// 自旋
for (;;) {
// 线程的前一个节点
final Node p = node.predecessor();
// 如果前一个节点是,继续尝试获取锁
if (p == head && tryAcquire(arg)) {
// 当前节点设置成头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 校验前一个节点的waitStatus是不是single
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 线程park
interrupted = true;
}
} finally {
if (failed) // 如果获取锁失败
// 设置node的状态为取消,后续unparkSuccessor时候会进行指针调整和垃圾回收
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前一个节点的waitStatus是否是SINGAL如果是就返回,线程就会执行parkAndCheckInerrupt
// 进行park,等待被唤醒或者被打断
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// 如果前一个节点的waitStatus状态是大于0,也就是CANCELED的,那么就将向前找到
// 没有打断的Node,并且将此Node的前一个节点指向那个没有打断的Node,其下一个节点
// 指向当前节点
// 流程结束,到外层继续自旋获取锁,如果拿不到继续设置前一个节点的waitStatus
/*
这边是在cancelAcquire时候,当不是tail节点时,会有一些多余数据无法释放,在这边就会
取消指针指向,从而垃圾回收
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 尝试去更改上一个节点的状态是Single,继续到外层自旋获取锁,如果拿不到继续设置前一个
// 节点的waitStatus,和上面 ws > 0 一致
// 设置前一个节点的waitStatus为SINGAL成功之后,再次自旋到ws == Node.SIGNAL,然后
// 线程就会执行parkAndCheckInerrupt,进行park
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 线程此时park,等待唤醒
LockSupport.park(this);
// 线程是否被打断过
return Thread.interrupted();
}
其实到这边正常流程就已经走完了,可以大概梳理下,一个新的线程尝试获取锁时,如果获取失败,那么就要进入队列中;在进入队列的过程中,先加入到队列的尾部并设置成tail,并将指针改变。
然后在acquireQueued中继续尝试获取锁:
1. 如果前面一个节点是head,那么继续尝试获取锁,可以想象一下,这边为什么是前一个指针是head时候才获取锁,因为这是一个双向链表,每次unpark时候都是向后unpark。此时这个线程既然能运行,那就有可能是head把它unpark的,说明head可能已经释放了,所以这边再次尝试获取下锁。
2.如果获取不到,那么就检查前一个节点的waitStatus是不是SINGAL,如果是则线程park。如果不是则尝试把前一个节点设置成SINGLE,在每次设置时候会尝试继续获取锁,因为可能这段时间他的前一个节点变成了head,自己可以获取锁了。
3. 前一个节点设置完waitStatus为SINGAL之后,此节点线程就可以park了,等待别的节点唤醒,或者被打断。
整理玩了正常的获得锁的流程,看下异常或者取消的时候的流程。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
// 将当前节点指向成没有取消过的前任节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 前一个节点的下个节点
Node predNext = pred.next;
// 设置当前节点的状态为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前node是tail,那么设置成前一个node为tail
if (node == tail && compareAndSetTail(node, pred)) {
// 设置新的tail的next为null
// 那么当前的node就没有被引用了,就会被下一次垃圾回收回收掉
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 如果当前节点前一个节点不是head
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 当前节点的下一个节点
Node next = node.next;
// 如果next是空说明到了tail,或者后面的被取消了也都没有影响
// 在shouldParkAfterFailedAcquire那里会进行调整Node的指针
if (next != null && next.waitStatus <= 0)
// 更新前一个节点的下一个节点为当前节点的下一个节点
compareAndSetNext(pred, predNext, next);
} else {
// 如果当前是节点的上一个节点是head,尝试唤醒下一个没有取消的线程
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 找到下一个节点,如果下一个节点是空,或者是取消的,就从后向前查找节点
// 其实这边有两个问题?
// 1. 为什么是从后向前去唤醒
// 2. 如果后面的被唤醒了,那么公平锁岂不是不公平了?
// 后来仔细又阅读了下源码发现了答案
// 1. 为什么要从后向前去唤醒呢?其实我个人认为可能是因为addWaiter时候设置tail时候不是
// 原子性的,先是node.prev = tail,然后设置新的tail,最后才把之前的tail的next更新到新的
// tail上。这就会无法找到新加的那个node
// 2. 后面的线程会唤醒,会不会导致公平锁不公平。答案是不会为,因为这时候如果被唤醒的话,
// 此时继续接着去走parkAndCheckInterrupt,然后再去自旋获取锁,如果它前一个节点不是head,
// 那么它将继续park,并不会拿到锁
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 线程unpark
LockSupport.unpark(s.thread);
}
附上cancelAcquire后节点的变更。
当节点是tail时:
当节点的前一个节点不是head时,在cancelAcquire后,Node2的前一个正常节点的next会变成Node3。当Node3被唤醒时,会尝试获取锁,如果前面节点不是head的时候,会继续执行shouldParkAfterFailedAcquired,然后将前一个节点设置成Node1节点。
当前一个节点是head时,直接唤醒下一个线程,当前node的waitStatus设置成CANCELLED,将下一个Node线程唤醒。下一个线程在唤醒之后尝试获取锁,由于前面一个节点不是head,所以继续执行shouldParkAfterFailedAcquired。
释放锁
前面分析了获取锁,后面释放锁就变的很简单了。
public final boolean release(int arg) {
// 尝试释放锁,由继承后实现,也就是维护state
// 例如在ReentrantLock中state = 0 就可以释放锁
if (tryRelease(arg)) {
Node h = head;
// 唤醒下一个节点
// 如果waitStatus == 0,那么可能是新加如进来没有后续节点或者是在unparkSuccessor时修
// 改成了0,不需要再去通知
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
至此AQS的排他锁的获取和释放就分析完了,由于能力有限而且才刚开始去读AQS源码,可能其中有些问题欢迎指正。