1 释放锁
释放锁的触发时机就是我们常用的Lock.unlock()方法,目的就是让线程释放对资源的访问权。
释放锁也是分为两类,一类是排它锁的释放,一类是共享锁的释放,我们分别来看下。
1.1 释放排它锁 release
排它锁的释放就比较简单了,从队头开始,找它的下一个节点,如果下一个节点是空的,就会从尾开始,一直找到状态不是取消的节点,然后释放该节点,源码如下:
// unlock 的基础方法
public final boolean release(int arg) {
// tryRelease 交给实现类去实现,一般就是用当前同步器状态减去 arg,如果返回 true 说明成功释放锁。
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空,并且非初始化状态
if (h != null && h.waitStatus != 0)
// 从头开始唤醒等待锁的节点
unparkSuccessor(h);
return true;
}
return false;
}
// 很有意思的方法,当线程释放锁成功后,从 node 开始唤醒同步队列中的节点
// 通过唤醒机制,保证线程不会一直在同步队列中阻塞等待
private void unparkSuccessor(Node node) {
// node 节点是当前释放锁的节点,也是同步队列的头节点
int ws = node.waitStatus;
// 如果节点已经被取消了,把节点的状态置为初始化
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 拿出 node 节点的后面一个节点
Node s = node.next;
// s 为空,表示 node 的后一个节点为空
// s.waitStatus 大于0,代表 s 节点已经被取消了
// 遇到以上这两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的
if (s == null || s.waitStatus > 0) {
s = null;
// 这里从尾迭代,而不是从头开始迭代是有原因的。
// 主要是因为节点被阻塞的时候,是在 acquireQueued 方法里面被阻塞的,唤醒时也一定会在 acquireQueued 方法里面被唤醒,唤醒之后的条件是,判断当前节点的前置节点是否是头节点,这里是判断当前节点的前置节点,所以这里必须使用从尾到头的迭代顺序才行,目的就是为了过滤掉无效的前置节点,不然节点被唤醒时,发现其前置节点还是无效节点,就又会陷入阻塞。
for (Node t = tail; t != null && t != node; t = t.prev)
// t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒
if (t.waitStatus <= 0)
s = t;
}
// 唤醒以上代码找到的线程
if (s != null)
LockSupport.unpark(s.thread);
}
网友评论