基本思路
通过cas去获取主动权,失败就去排队.持有锁的解锁后唤醒下一个, 被唤醒后的线程看看自己是不是排到了第一.
值得注意的是,排队时候是可以取消的.从队列中删除某个节点,不能出现死循环,指向错误的问题.
由于支持节点的取消:
- next是不可靠的, 只能使用prev去遍历
- 遍历可能是不完全的,只靠release的时候去唤醒是不够的,取消的时候也要支持唤醒
本文将描述三个部分: cas, 排队, 唤醒.
基本上就是解释下面这个代码:
public final void acquire(int arg) {
//tryAcquire 是一个cas操作 不成功就入队
//addWaiter 入队 需要注意多个线程同时入队的操作
//acquireQueued 判断自己是不是第一,不是的话继续park
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果中途被发送了中断信号,那就中断自己
selfInterrupt();
}
cas
cas全称为compareAndSet. 比较和设值是一个原子操作, 例如cas(v, 1, 2), v是我要设值的变量, 1 为期望值, 2 为目标值. 只有v在等于1的时候能够被设置为2且返回true,其余时候都返回false.
请务必保证v被volatile修饰,各线程才能正常竞争.
tryAcquire
有了cas,获取锁的操作就很简单.写了一个无法排队和阻塞的锁.
public class MyLock {
/**
* 记录状态,每lock一次,就+1,反之-1
*/
private AtomicInteger state = new AtomicInteger(0);
/**
* 记录谁持有这个锁
*/
private Thread current = null;
/**
* 尝试获取,非阻塞
*/
public boolean tryLock() {
if (current == Thread.currentThread()) {
//本线程已经持有该锁,重入+1
state.addAndGet(1);
return true;
} else if (state.compareAndSet(0, 1)) {
//依靠cas最多只让一个线程到达这里
//当然可以多判断一下state目前是不是等于0再cas
current = Thread.currentThread();
return true;
}
return false;
}
public void unlock() {
//没有持有该锁的线程进行unlock抛错
if (current != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
if (state.get() == 1) {
//提前解绑 再把state置为0
current = null;
}
//在java里面,只能通知一个线程中断,不存在抢占式中断.所以不用担心这里不能设置为0
state.addAndGet(-1);
}
}
但aqs的重点并不是cas,是排队,cas是一种同步的手段,只让一个线程操作成功.
在ReentrantLock的实现中,有公平锁和非公平锁两种实现,顾名思义,公平锁不允许插队.已经有人在排队了,则只能添加到队列末位. 而非公平锁则允许插队.
非公平锁存在饿死的情况,老是被插队,队列中的线程得不到执行的机会.但是和公平锁相比,在刚好锁可用的情况下,减少了一次线程的入队阻塞和一次线程的唤醒.性能会更好.
ReentrantLock中的state是一个计数, 只要state>0,肯定有一个独占线程.state==0,独占线程为null.
//公平
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//如果没人排队,并且cas成功才能获取到锁
//是否有人排队的判断就算不够及时也没有关系,本线程进入排队后会立刻查看自己是否在第一位,如果是的话,就会获取到锁
if (!hasQueuedPredecessors() &&
//很多线程可以同时执行到这里.大家都认为没人排队
//需要cas同步,只让一个人通过,否则会有多个线程同时获取锁
//这个cas的时候,可能有人在排队了,也可能没人在排队但有人获取了锁,不过state都会是大于0
//也有可能在短时间内,有人获取了锁又释放了,产生了aba问题,这时候是能成功cas的,aba在这个场景没有副作用
compareAndSetState(0, acquires)) {
//设置独占线程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//如果已经持有 重入
int nextc = c + acquires;
//达到了最大值,溢出
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//非公平
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//唯一的区别在于不需要判断是否有人在排队,其他是一样的
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
排队
排队的第一步是生成一个Node入队.aqs维护head和tail,队列为一个双向链表.
每个node需要对应一个thread,也需要记录对应的状态.
状态有
- cancelled = 1 表示被取消 被取消的node需要被移除队列
- 移出队列有几种方法,一种是被别人跳过,一种是取消时自我移除. 自我移除的同时,如果前面节点同时在取消,那么会失败
- signal = -1 表示需要唤醒下一个node,换句话说:后面的node在这里打个标记,告知别人这个node后面有需要唤醒的node
- condition = -2 表示在wait condition
private Node addWaiter(Node mode) {
//acquire的mode是独占
Node node = new Node(mode);
for (;;) {
//当前的tail
Node oldTail = tail;
if (oldTail != null) {
//prev指向旧tail,这时候tail可能是发生变化的
node.setPrevRelaxed(oldTail);
//将tail通过cas设置成当前node 只有tail不变的情况下,才能设置成功
//如果失败,有两种情况
//一种是别的线程要入队比本线程更早成功
//一种是tail节点被取消,而被移除队列
//当然也可以存在一个node成为新tail然后又取消出队的情况,aba没有副作用
if (compareAndSetTail(oldTail, node)) {
//成功后,oldTail就是前一个节点了,将next指向新的tail
//如果这个时候oldTail被移除队列了,其实没有关系
//因为前面可用的node指向了本node.而oldTail的引用将不会有人再持有,等待垃圾回收
oldTail.next = node;
return node;
}
} else {
//队列还不存在,初始化一下
//初始化也是通过cas(null, new Node())设置head, tail=head
//也可能设置失败,被别的线程先设置了
//头结点只是一个空结点
//无论如何,都需要进入下一个循环去排队
initializeSyncQueue();
}
}
}
下图简单的表示两个线程想要排队的情形,是cas的直观表述,下图不包括队列为空,有人取消node等情形:
入队后的node(thread),会在一个循环中,判断自己是不是排到了第一个,不是的话就挂起
//参数node就是入队时生成的node
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
//node自旋
for (;;) {
//前一个node
final Node p = node.predecessor();
//如果前一个node是head并且抢到了主动权
//按道理我被唤醒只要前一个是head(head是一个空结点),就轮到我执行了
//公平锁的情况下,我肯定能tryAcquire成功
//但是非公平锁的情况下,就算我的前一个是head,tryAcquire不一定是会成功的
//因为非公平锁,别的线程可以不考虑队列是否为空就去tryAcquire,本线程是和别的线程一起竞争
if (p == head && tryAcquire(arg)) {
//如果我获取成功了,那我已经获取了锁了
//把本节点设置为head,并且将thread置空,成为空节点
setHead(node);
p.next = null; // help GC
return interrupted;
}
//否则判断一下是否需要park
//虽然我的前面一个node不是head,但是可能前面的节点都是被取消的
//我只要清理掉取消的node,我的前一个node就可能是head了
if (shouldParkAfterFailedAcquire(p, node))
//native park 等待被唤醒
//可能是中断唤醒的,而不是被正常唤醒的
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
//出现问题 取消
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
//判断是否需要park
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//已经做过清理打过标记了,安心去park
//但是假设我排在第二,我先判断第一节点是signal的,然后park了,接着持有锁的线程释放锁,第一个节点被unpark
//恰好这时候第一节点又被取消了,unpark会拿到一个取消结点,那么按照顺序本节点应该要被唤醒
//所以unpark需要遍历去找到第二个节点,next是不可靠的,只能靠prev去遍历
//但prev遍历也会存在无法遍历全节点的问题
//假如我是在prev遍历的时候加入的,遍历时队列只有第一个取消结点,接着加入了第二个可用结点,那么遍历会认为队列是空了
//所以需要第一个节点取消的时候去唤醒第二个节点,所以取消操作的时候,如果前一个节点是头结点,需要唤醒下一个可用结点
return true;
//前一个node是取消的
//就算做过清理了,清理后可能被置为取消.cas为signal没有成功
if (ws > 0) {
//如果前置节点是取消状态,那么清理掉前面连续的取消结点.
//如果前面的node被取消,并且在做自我清理,不会影响到这里,当然自我清理可能失败
//一个节点不能从取消回到非取消状态,那么只要是取消就可以跳过
//自我清理失败的结果是:prev指向了前面,而前面对应的next没有指向取消结点的下一个结点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//这里是唯二能够真正清理掉取消结点的地方
//还有一个是cancel的时候,把前一个可用结点的next做cas操作指向后面
pred.next = node;
} else {
//这时候,这个节点可能在取消,那么ws就会是cancel,就不能设置为signal
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
//那就再来一次
return false;
}
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
//置空 成为空node
node.thread = null;
// 跳过被取消的node
// 但是停下的地方可能一会儿也被取消
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//pred取消了, predNext可能是pred
//也可能一会儿发生改变
Node predNext = pred.next;
//置为取消状态
//可能有别的结点把这个结点设置成了signal,取消就是了
//但这时候后面的节点先判断是signal然后去park了,结果这里又被设置为取消,那么后面的节点可能需要被唤醒
//是否要唤醒,依赖于前方是否还有可用结点,如果有的话,那就不需要了,如果没有,那就是需要的
node.waitStatus = Node.CANCELLED;
//如果是尾结点,将最后一个非取消结点置为尾结点
//如果有新的线程入队,cas将会失败.但是入队操作会帮我们把node的next指向了新的tail,这样就把node留在了队列里.
//不过新的入队线程会马上把我们给跳过清理掉
//这里也可能aba,新线程入队,马上被取消出队,没有副作用
//pred这时候可能是取消的
if (node == tail && compareAndSetTail(node, pred)) {
//把尾结点的next置为null
//如果这时候又有新的入队,则next已经不是predNext,将会失败
//或者pred取消了,指向了自己
//或者pred取消前新线程入队,新线程还未将oldTail的next指向新node,pred取消后next指向自己,然后又被新线程指向了新的tail
pred.compareAndSetNext(predNext, null);
} else {
int ws;
//如果前一个有效结点不是头结点 尝试把前一个有效结点置为signal
//如果前一个节点这时候取消了,那么就会失败
//能置成功或者本来就是signal的,说明前面这个结点没有被取消,否则就不清楚前方什么情况
//那么需要调用unpark,指不定前方没有节点可用了
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL)))
&& pred.thread != null) {
//进到这里只是为了确定,前一个节点此时没有被取消
//如果进来后前一个结点要取消或者取消到一半,不会影响到后面的操作
//每一个取消操作,都可能调用unpark,所以我只保证前面有节点可用,我就不会去unpark
//如果没有进来,说明我不知道前面是否有节点可用,保守起见,我调用unpark
Node next = node.next;
//尝试摘掉自己和前面所有的取消结点
if (next != null && next.waitStatus <= 0)
//pred的next可能指向了自己,也可能指向了更后面的节点
//1 pred取消后指向了自己
//2 有更后面的取消结点比我更先操作pred,那么pred会指向更后面,比如pred成了尾结点
pred.compareAndSetNext(predNext, next);
} else {
//前一个节点可能是head,那么需要唤醒下一个node
//这里的这个操作是必要的
//只靠release来唤醒是不够的 如果刚好有新线程在入队,那么release操作就无法遍历到新线程去唤醒
//另一方面,不清楚前方状况,全局看一下
unparkSuccessor(node);
}
//如果这时候后面的节点都被取消了,这个结点成了尾结点,这个next可能在新线程入队的时候又被指向了新的tail
//也可能在shouldPark往前跳过取消结点时,赋值给下一个结点
//如果这里不做这个操作,同时acquireQueued函数里,head.next不被置为null,next遍历也会是可靠的??
//只要next不会指向前面的结点形成循环,不会被置为null
node.next = node; // help GC
}
}
唤醒
release后,将自己置为signal,唤醒下一个node.如果没有人在排队,直接返回.
tryRelease
protected final boolean tryRelease(int releases) {
//和tryAcquire相反的操作 就不会区分公平非公平了,因为只有一个线程获取了锁
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
//先置空线程
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
public final boolean release(int arg) {
//只有之前acquire成功的才能tryRelease成功
if (tryRelease(arg)) {
Node h = head;
//如果acquire是在队列空的时候,不用排队,也就没有head
if (h != null && h.waitStatus != 0)
//如果有队列的话,唤醒head下一个node
unparkSuccessor(h);
return true;
}
//如果这时候有人排队了,要靠它自己入队时的自旋来获取锁
//就不是触发式的
return false;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
//清理状态
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
//这里node.next可能会是node自身
Node s = node.next;
//下一个结点是个空或者在取消
if (s == null || s.waitStatus > 0) {
s = null;
//从后往前遍历找到最前面的
//如果是从前往后去找,找到某个节点后,这个结点被取消,然后next指向了自己,就悲剧了.
//如果有新线程入队,这个新线程不能被遍历到,这点会带来一个隐患,是node自旋无法解决的:
//考虑p赋值为tail后, 新线程入队,然后新线程查看自己是不是第一位
//不是第一位,然后尝试往前跳过取消结点,发现依然不是第一位
//这时候新线程被挂起,接着前面的节点都取消了
//这时候除了取消操作里去唤起,没有别的操作能唤起这个线程了
//所以cancel的函数里是需要调用本函数的
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
//可能队列空了,可能是新线程没有被遍历到
if (s != null)
LockSupport.unpark(s.thread);
}