RenntrantLock中unlock过程解析

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
try {
lock.lock();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
}

直接点进unlock方法,这个解锁和加锁不一样不分公平非公平之说。

1
2
3
public void unlock() {
sync.release(1);
}

查看release方法

release方法

1
2
3
4
5
6
7
8
9
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head; // 获得当前队列的头结点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 这个方法是释放锁中的重头戏最为重要,后面详细说明。
return true;
}
return false;
}
/**
 * h != null 判断队列头是否为空,如果head执行null,说明当前并没发生竞争,只有一个线程访问或者线程交替访问简短来说就是发生线程竞争的情况
 * h.waitStatus != 0 需要满足这个条件是因为如果头线程的waitStatus为0的话只能说明线程中曾发生过竞争,但是现在已经不存在需要唤醒的结点了,就是当前线程中只有头结点这一个结点。
 * 你不了解为什么这样的话可以看加锁的过程,后一个结点会把前一个结点的状态设置为-1。然后再根据后面说明的唤醒线程的具体处理过程unparkSuccessor(h)方法看一看。
 */

先看tryRelease这个方法,这个和加锁中的那个tryAcquire方法一样的,一个尝试解锁,一个尝试获取锁。都是模板方法的体现,具体的实现交由子类重写。

tryRelease方法

1
2
3
4
5
6
7
8
9
10
11
12
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 获取原来的锁的状态减去释放的 重入的一种体现
if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前要尝试解锁的线程和持有锁的线程是不是一样,不一样则就出现错误了!!
throw new IllegalMonitorStateException(); // 抛出异常程序终止
boolean free = false; // 这个变量代表是否释放锁成功,false代表失败,true成功,看下面代码就知道了。。
if (c == 0) { // 如果锁释放后,c=0的意思就是锁的状态变为未被任何线程持有状态
free = true; // 释放成功,将free设置为true。
setExclusiveOwnerThread(null); // 将当前持有锁的线程设置为空,因为没有线程持有锁了,肯定要设置null啊。
}
setState(c); // 设置锁的状态
return free; // 返回当前线程是否成功释放锁!
}
1
2
3
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}

这个方法只是简单的释放锁而已并不是释放锁的重头戏,唤醒线程释放锁才是重头戏。这个tryRelease方法也体现了ReentrantLock锁的重入性。

这个方法执行完毕后返回free,如果释放成功,返回true,看release方法接着向下执行。

unparkSuccessor方法(※)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; // 获取头结点的waitStatus
if (ws < 0) // 头结点的ws小于0,存在需要唤醒的结点
compareAndSetWaitStatus(node, ws, 0); // 将头结点的ws设置为0

Node s = node.next; // 获取头结点的下一个结点
if (s == null || s.waitStatus > 0) { // 如果头结点的下一个结点为null,或者头结点的下一个结点的ws>0,都是些不正常的情况
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 这个for循环是从线程的尾结点一直向前便利直到获得最后一个等待唤醒的结点。(这里的最后如果从前往后就是第一个,从后往前不就是最后了吗hh)
if (t.waitStatus <= 0)
s = t;
}

if (s != null)
LockSupport.unpark(s.thread); // 唤醒这个队列中第一个waitStatus<0的那个线程
}

线程被唤醒之后

不知道还知不知道这个被唤醒的线程当时最后被阻塞哪里!!!

1
2
3
4
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞当前线程
return Thread.interrupted();
}

被唤醒的那个线程当时被阻塞到了这里,被唤醒之后肯定要尝试获取锁,走的还是加锁的那套流程,知道在哪阻塞了加锁之前那套也说的很明白。可以看之前的加锁过程来了解,这里简单的说明。

return Thread.interrupted();这个返回值不用太在意,返回线程的一个中断状态,阻塞的时候是否被中断了,这里很细节暂时不用管。只知道返回了true或者false,程序继续执行,又进入一次for循环,因为锁已经是空闲状态所有,这里尝试获取锁会成功,然后将原来的头结点干掉,当前节点变为原来的头结点。

这也就说明了unparkSuccessor中为什么直接ws设置为0,这个当前节点也有两种情况。

一种为<0,说明线程中还存在需要被唤醒的结点,它释放锁后和这个unlock路线一样的。

还有一种情况是状态为0,说明当前节点是这个队列中最后的那个结点(因为最后那个结点的状态才会是0),队列中也自然就不存在需要被唤醒的结点了啊。

总结

解锁过程就是将这个锁的状态设置为0->将当前持有锁的线程设置为null->唤醒队列中的线程->被唤醒的线程执行加锁的过程。。。。明白了加锁过程解锁应该很简单。