《Java源码分析》:ReentrantLock.unlock 释放锁
上篇博客对ReentrantLock类中加锁的过程进行了一点介绍,可以在这里看到:http://blog.csdn.net/u010412719/article/details/52083731。
本篇就对ReentrantLock.unlock释放锁进行一点点记录。
ReentrantLock.unlock()
API给出的介绍为:
试图释放此锁。
如果当前线程是此锁所有者,则将保持计数减 1。如果保持计数现在为 0,则释放该锁。如果当前线程不是此锁的持有者,则抛出 IllegalMonitorStateException。
下面就从源码的角度来分析如何释放锁。
ReentrantLock.lock方法如下,直接调用了AQS类中的release方法。
public void unlock() {
sync.release(1);//直接调用release方法
}
AQS类中的release方法的代码如下:
/*
函数功能:以独占的方式释放,如果tryRelease方法返回true则能消除一个或多个线程变为非阻塞
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {//释放成功则将头结点的既任节点唤醒,头结点代表的就是拥有锁的节点
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
代码思路:
如果调用tryRelease释放锁成功,返回true。则调用unparkSuccessor函数来将头结点的继任节点唤醒。
下面我们来具体看release(int arg)方法中的tryRelease(int arg)和 unparkSuccessor(Node node).
tryRelease(int arg)
对于独占锁,函数tryRelease的参数arg == 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);
}
//设置状态
setState(c);
return free;
}
代码分析
1、先检查当前线程是不是拥有锁的独占线程。如果不是,则抛异常。这是因为线程不能够释放其它线程所拥有的锁。如果是,则进行 2.
2、将AQS状态位减少要释放的次数(对于独占锁次数为1)。如果剩余的状态位为 0(也就是没有线程持有锁).那么当前线程是最后一个持有锁的线程,调用setExclusiveOwnerThread来设置AQS持有锁的独占线程为null.接着进行 3
3、将剩余的状态位写会AQS,如果没有线程持有锁,则返回true,否则返回false
这里c==0决定了是否完全释放了锁。由于ReentrantLock是可重入锁,因此同一个线程可能多重持有锁,那么当且仅当最后一个持有锁的线程释放锁是才能将AQS中持有锁的独占线程清空,这样接下来的操作才需要唤醒下一个需要锁的AQS节点(Node),否则就只是减少锁持有的计数器,并不能改变其他操作。
当tryRelease操作成功后(也就是完全释放了锁),release操作才能检查是否需要唤醒下一个继任节点。这里的前提是AQS队列的头结点需要锁(waitStatus!=0),如果头结点需要锁,就开始检测下一个继任节点是否需要锁操作。
我们在上篇博文中知道acquireQueued操作完成后(拿到了锁),会将当前持有锁的节点设为头结点,所以一旦头结点释放锁,那么就需要寻找头结点的下一个需要锁的继任节点,并唤醒它。
unparkSuccessor(Node node)方法
方法的源码如下,
函数功能:找到当前节点的下一个需要锁的继任节点,并将调用unpark将其唤醒。
private void unparkSuccessor(Node node) {
//此时node是需要是需要释放锁的头结点
int ws = node.waitStatus;
if (ws < 0)//清空头结点的waitStatus,也就是不再需要锁了
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//从队列的后面开始寻找第最后一个waitStatus<=0的节点,然后唤醒,为什么不是从前往后,而是从后往前,不知道。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果找到一个有效的继任节点,就唤醒此节点线程
if (s != null)
LockSupport.unpark(s.thread);
}
//LockSupport类中的unpark方法如下
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
上面代码的思路比较简单。就是找出头结点的下一个需要锁的继任节点然后唤醒。
为什么在unparkSuccessor()函数中是
从队列的后面开始寻找第最后一个waitStatus<=0的节点,然后唤醒,为什么不是从前往后,而是从后往前??
这个目前还没有找到答案
以上就是释放锁的全部过程。
小结
unlock释放锁的大致思想:如果当前线程释放锁成功,则寻找头结点的下一个需要锁的继任节点并唤醒。如果释放锁不成功,则什么也不做。
参考资料
1、http://www.blogjava.net/xylz/archive/2010/07/08/325540.html
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。