「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」
addWaiter 新增节点
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
//插入节点,头节点/尾节点都是一个空节点,必要时初始化队列
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 初始化队列
if (compareAndSetHead(new Node())) //设置头节点
tail = head;
} else {
node.prev = t;
// 将新的节点设置成最后的节点的下一个
if (compareAndSetTail(t, node)) { // cas将节点设置为尾节点,如果别的线程使用了unpark()从后面遍历,不会因为关系没有建立成功而失败
//分析该节点的前置节点在cas之前就执行成功了,如果在cas执行期间,其他线程执行了unpark()了,从前面遍历 而恰巧这时候t.next =node 还没执行,关系没建立好,就会出现
t.next = node;
return t;
}
}
}
}
复制代码
流程说明
1、先创建一个节点,如果尾节点不为空,说明之前这个队列已经实例化好了,直接将新创建的节点通过cas设置成尾节点即可。
2、如果尾节点是空的,说明等待队列还没有实例化出来,先要实例化一个节点 enq(), enq()函数就是创建等待队列的,头节点是个空节点。 注意在cas的操作的时候,新节点的前置节点,和后置节点是分开的。
acquireQueued aqs管理节点
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);//头节点设为自己,节点设置:node.prev = null node.thread = null,头节点都是一个空节点
p.next = null; // 前任节点 = null 垃圾回收 hlpe gc
failed = false;
return interrupted;
}
// 如果非头节点或者获取锁失败
//1、如果是新节点,第一次回返回false 第二次判断正常返回true
//2、parkAndCheckInterrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
复制代码
流程总结:
for循环中
1、先获取node节点的前置节点,判断是否为head节点,如果是head节点那么就去抢锁,如果成功了获取到锁,返回。
2、如果获取失败或者前置节点不是head
3、shouldParkAfterFailedAcquire() 的作用就是来给该节点设置"闹铃的",这个闹铃的意思是:该线程堵塞后,谁来唤醒该节点?
4、第一次上闹铃是不成功,只会找到合适的前置节点,这里的合适节点,比如该节点的前置节点是过期的或者无效的,那么就要跨过该节点,找个这个节点的前置节点。
5、成功之后,调用LockSupport.park()将该线程进行堵塞。
以上过程就是上锁的流程,我们再次进行规整一下:
获取锁的总结
1、如果是非公平第一次进来会通过cas设置state(0,1) 如果设置成功那么就获取锁,否者执行获取锁的流程,而公平锁不会去枪锁,直接走获取锁的流程
2、先执行tryAcquire(),公平和非公平的差距在于,如果这时候没有线程持有锁,非公平会直接枪锁,而公平会多一步查看等待队列是否有线程在排队。
如果这时候锁被其他线程持有那么获取锁失败,如果判断获取锁的线程是自己那么是自己state+1(可重入)
3、tryAcquire失败后,执行addWaiter(),构建一个新节点,这时候会判断如果队列还没有初始化,会初始化一个队列(第一个节点是个空的,设置为新增节点的头节点),末尾节点cas成新的节点,并返回lastNode(自己)。
acquireQueued()AQS对生成的节点进行管理:下面是循环获取该节点的前置节点,如果是头节点(空)并且获取到锁了,那么将自己设置头节点(里面的属性部分清空,thread),前置节点设置null。
成功之后返回该线程是否被中断。如果没有抢锁成功 或者不是前置节点不是头节点,分两步判断:
1、该节点的前置节点是否为signal,因为本线程要进行线程挂起,让前面的线程进行叫醒。如果前面的线程不是signal,那么第一次会将前面线程设置成signal,返回false, 新的一轮继续,否则是true
2、前面那一步是true的话,说明该线程进行堵塞,如果被前置节点唤醒之后,再次进行判断,最终返回出去true
所以后面的情况会循环两次
注意点:非公平锁可能会抢多次锁,而非简简单单得一次,不管是等待队列还是堵塞队列,基本上头节点算是一个空的,但是在解锁得时候还是通过头节点判来释放的
复制代码
unLock() 释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) { //释放锁成功
Node h = head;
if (h != null && h.waitStatus != 0) //后面还需要唤醒
unparkSuccessor(h);
return true;
}
return false;
}
//释放锁:获取state,判断是否<0 如果等于0 说明成功,否者就是重入锁减-1
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);//锁持有线程设置null
}
setState(c);
return free;
}
复制代码
释放锁流程总结:
1、state -1 等于0 释放锁结束
如果头节点不为空且状态>0这个头节点是获取到锁的线程,如果这个线程在等待队列中排队,那么后面第一个线程会设置个表示signal,这个相当于是闹铃,后面线程挂起,如果你释放了麻烦叫叫我,
如果有这个需求就会唤醒后面的线程,是从后面倒序唤醒,为什么?
个人理解因为在获取的时候会把head节点的next去掉,找不到下一个节点。
复制代码




近期评论