Java并发-- ReentrantLock 可重入锁实现原理2 - 释放锁
Posted Hepburn Yang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发-- ReentrantLock 可重入锁实现原理2 - 释放锁相关的知识,希望对你有一定的参考价值。
接着上一篇分析……
给一扇传送门,【Java并发】-- ReentrantLock 可重入锁实现原理1 - 获取锁
当ThreadA线程执行完任务后调用finally中的unlock()方法释放锁的时候会经历什么样的操作。
ReentrantLock.unlock()
文章目录
1. ReentrantLock中的unlock()
/*
* 释放锁
*/
public void unlock()
sync.release(1); // sync是继承了AQS的静态内部类
2. AQS中的release()
public final boolean release(int arg)
if (tryRelease(arg)) // 释放锁成功
Node h = head; // 获取到AQS队列中的头结点
if (h != null && h.waitStatus != 0) // 如果 头节点不为空 并且状态!=0.
//调用 unparkSuccessor(h)唤醒后续节点
unparkSuccessor(h);
return true;
return false;
这里面有两个核心方法,这么机智的你肯定一眼就看出来了,就是tryRelease()和unparkSuccessor();
3. AQS 中的tryRelease(),
嗯哼,这块和tryAquire()是一样的,都是模板方法模式的体现,都没有给出具体的实现,真正的tryRelease() 实现还是需要在子类,也就是ReentrantLock的内部类sync中重写tryRelease();
protected boolean tryRelease(int arg)
throw new UnsupportedOperationException();
4. ReentrantLock中的tryRelease()
这个方法可以认为是一个设置锁状态的操作,通过将state状态减掉传入的参数值(参数为1),结果状态为0,就将独占锁的owner设置为null,方便其他线程有机会抢占锁;
- 在独占锁加锁的时候,我们会将state状态+1,不清楚的看上一篇获取锁的过程,里面有分析到;
- 所以,同一个锁,在重入多次之后,可能被叠加多次,比如重入了4次,最后state的值就是4,getState()得到的结果就是4;
- 在进行unlock的时候,就需要减掉所有的重入次数,才能完全释放锁;
- 也就是unlock()的次数与lock()次数对应上,才能将owner设置为null,释放掉锁,最后返回true;
/*
* 释放锁
*/
protected final boolean tryRelease(int releases)
int c = getState() - releases; //getState获去到总重入次数减去1
if (Thread.currentThread() != getExclusiveOwnerThread()) // 判断当前线程是否是获取到锁的线程,如果不是就抛异常 (只有获取到锁的当前线程才能释放锁!!)
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) // 最后减到为0表示释放完毕
free = true;
setExclusiveOwnerThread(null);
setState(c); // 更新重入次数,直到为0 ,否则return false,继续走释放的逻辑
return free;
5. AQS的unparkSuccessor()
直到tryRelease()返回true,完全释放锁成功后,我们会调用AQS的unparkSuccessor()唤醒后继节点,英文注释都是大师Doug Lea写的,为了不曲解他的本意,放着让大家看看;
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
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; // 获得 head 节点的状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 设置 head 节点状态为 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; //得到 head 节点的下一个节点
if (s == null || s.waitStatus > 0)
//如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
//通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
if (s != null)
//next 节点不为空,直接唤醒这个线程即可
LockSupport.unpark(s.thread);
为什么这里是从tail扫描找距离head最近的节点,从head开始扫描的不是更近更快吗?
上一篇分析的enq()构建节点方法里面,最后一步是 t.next=node,设置原tail的next节点指向新的节点,若在 cas 操作之后, t.next=node 操作之前。 存在其他线程调用 unlock 方法从 head开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断,因为tail还没有指向新的尾结点。 所以若从后往前遍历,一定不会存在这个问题。
6. 挂起的线程继续执行
脑补一下上篇文章画过的图,现在ThreadA已经释放完锁,也唤醒了后置节点ThreadB,所以ThreadB要继续执行,那它会从哪开始执行呢?
思考一下ThreadB被阻塞到哪了?
是的,被阻塞在lock()中调用的AQS.acquireQueued()中了。
下面我们关注一下ThreadB 被唤醒以后的执行流程;
- 把 ThreadB 节点当成新的 head
- 把原 head 节点的 next 节点指向为 null,断开ThreadA
以上是关于Java并发-- ReentrantLock 可重入锁实现原理2 - 释放锁的主要内容,如果未能解决你的问题,请参考以下文章
Java并发-- ReentrantLock 可重入锁实现原理1 - 获取锁