该篇主要以分析AQS的源码为主,从源码来看AQS是如何实现在独占模式和共享模式下的加锁、释放锁的逻辑。
目录
独占模式
1.获取锁
1.1不可中断
1.acquire,是获取锁的入口方法
//获取锁(不会被线程中断影响)
public final void acquire(int arg) {
//先尝试获取锁,未成功会调用不能被打断的自旋获取锁的方法
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
//如果获取锁成功后,发现该线程已经是中断状态,则调用该线程的中断方法
selfInterrupt();
}
}
- 该方法会先去尝试获取锁(tryAcquire,由子类自定义实现)
- 成功则调用结束,失败则调用acquireQueued方法,将线程放入同步队列自旋的获取锁
- 当线程自旋获取锁成功后会返回在自旋过程中线程有没有被标记过中断,如果有则调用线程中断方法
2.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方法,自旋直到节点插入到队列尾节点为止
enq(node);
return node;
}
- 创建节点node,通过传入的参数mode(值为SHARED或EXCLUSIVE)来区分节点是独占模式还是共享模式
- 尝试将node放入同步队列的尾部tail(通过CAS更新尾部节点信息)
- 成功则返回,失败则调用enq方法,通过自旋的方式不断尝试将node节点放入同步队列尾部,直至放入成功
3.enq,线程自旋进入同步队列的方法(该方法会不停的尝试将节点node放入同步队列尾部)
//死循环节点入队
private Node enq(final Node node) {
//CAS自旋,直到节点成功插入尾部
for (; ; ) {
Node t = tail;
if (t == null) {
//初始化节点
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
//将该节点的前任节点设置为队列的尾部节点
node.prev = t;
if (compareAndSetTail(t, node)) {
//将当前节点设置为尾部节点,并让前任节点指向该节点
t.next = node; //注意点
return t;
}
}
}
}
-
首先获取同步队列的尾部节点,如果为空,则说明同步队列为空,则创建一个空节点进行同步队列的初始化
-
先将节点node的prev指向尾节点
-
然后通过CAS尝试更改尾节点,更改成功,node就算成功进入队列了
-
然后在将上一个尾节点next指向新的尾节点node,这样就是一个完整的双向链表队列了
注意:上面代码中标注的注意点那一行,因为其本身是不具备线程安全的,所以可能存在一种情况,就是node节点成功放入队列尾部,但是没来得及更新next,线程就发生了切换,那么在线程重新获取到执行权限之前,上一个节点(t节点)的next都是null,无法指向新的节点。所以这也是为什么AQS中大部分都是尾节点遍历,因为这种极端情况下,无法顺利的从头节点找到尾节点,可能会漏掉部分节点。
4.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;
}
//shouldParkAfterFailedAcquire检查并清理失效的前任节点,如果前任节点为-1则返回true
//parkAndCheckInterrupt返回线程中断状态
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
//如果前任节点状态正常且当前节点线程为可中断状态
//则标记当前节点的线程需要被中断
interrupted = true;
}
}
} finally {
if (failed) {
//如果未获取到锁,且跳出了循环,则将当前节点设置为已取消并移出同步队列
cancelAcquire(node);
}
}
}
- 如果当前节点位于头节点(head)之后,则会尝试获取一次锁
- 若成功获取锁,则调用setHead方法,设置当前节点node为头节点(注意:setHead会将head设置为node,且会将node的prev和thread设置为null)
- 继续将旧头节点的next设置为空(此时旧头节点没有引用到任何节点,也不会被任何节点引用,相当于被剥离出队列了),然后返回线程的中断状态interrupted
- 若获取锁失败,则调用shouldParkAfterFailedAcquire方法和parkAndCheckInterrupt方法(这两个方法用于在节点线程获取锁失败后暂时阻塞该线程,并在线程被唤醒时返回线程是否被标记为中断)
注意:节点并不是在不停的获取锁,它会在不满足条件或者失败的时候调用shouldParkAfterFailedAcquire方法,该方法会将前任节点的状态改为-1(意为释放锁时需要唤醒后继节点),这样当前节点就不需要一直主动去获取锁,只需要休眠等待,在前任节点释放锁时来唤醒它,此时它再来获取锁即可,这也避免了大量无意义的性能消耗。(节点线程休眠等待时,想要唤醒该线程,只有其他线程调用LockSupport.unpark线程唤醒方法,或者调用interrupt线程中断方法)
5.shouldParkAfterFailedAcquire,判断获取锁失败后是否需要阻塞的方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果前任节点的状态是-1,则直接返回true
if (ws == Node.SIGNAL) {
return true;
}
if (ws > 0) {
//如果前任节点是已取消的
do {
//将前任节点的上层第一个未取消的节点设置为当前节点的前任节点
//相当于清理了失效的前任节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前任节点是有效的
//那么将前任节点的状态更改为-1,表示前任节点需要去唤醒后继节点
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
-
如果前任节点状态为-1,意味着当前节点可以让出CPU,阻塞等待至前任节点使用完锁来唤醒它即可,此时返回true
-
如果前任节点状态大于0(也就是1),说明该节点的前任节点已经因为某些原因取消了,成了失效节点。那么会从该节点起,向前遍历并清理失效的节点,直至遇到有效节点为止。此时暂时返回false(这种情况,一般会在下次或下下次遍历时返回true,类似于acquireQueued那里会多循环一两次后再休眠线程)
-
如果前任节点状态不为-1,也没有取消,那么会将这个前任节点状态以CAS更改为-1,让其在释放锁时去唤醒后继节点(以免后继节点需要不停的主动尝试获取锁,浪费CPU)。此时暂时返回false(不过下次循环时,该方法就会返回true了,该节点线程就可以阻塞等待了)
该方法一般是配合parkAndCheckInterrupt方法一起使用,只有该方法成功将前任节点更改为-1时,当前节点才能调用parkAndCheckInterrupt让自己阻塞,不然的话当前节点阻塞后没有节点来唤醒它,那么它将会一直阻塞在那里。
6.parkAndCheckInterrupt,线程阻塞等待的方法
pri