目录
① ReentrantLock的lock公平锁实现方法最终会到 tryAcquire ()方法
③ 以独占不间断模式获取已在队列中的线程:acquireQueued
前言
本文通过逐步分析ReentrantLock的源码(Java8)来学习其原理。
一、ReentrantLock总体概述
...
二、源码分析
1.ReentrantLock#lock 公平锁源码分析
逻辑:尝试获取锁,获取成功则直接退出;获取失败则将线程排队,排队后再中断线程。
ReentrantLock的lock公平锁实现方法会调用到 acquire (int arg)方法,该方法又包含了 tryAcquire() 、addWaiter() 、acquireQueued() ,下面就逐步分析下这几个方法。
public final void acquire(int arg) {
//获取锁成功:结束。
//获取锁失败:开始排队。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire():尝试获取锁。
addWaiter() :线程排队。
acquireQueued():独占不间断模式获取已在队列中的线程。
①尝试获取锁:tryAcquire()
该方法总体能概括为三步:
- 第一步:获取当前线程的状态state。
- 第二步:如果state为0,则当前线程还没有获取到锁。
- 第三步:如果state不为0时,当前线程已经拥有独占访问权限(即不用排队等操作,已经拿到了锁)。
获取到锁、没获取到锁之后的过程已经在下例源码中标出:
//尝试获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取当前线程的同步状态,state=0锁空闲,获取锁就会+1
int c = getState();
//没有获取到锁
if (c == 0) {
//如果当前线程之前没有排队线程则:1.通过CAS设置state
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//2.设置当前线程为拥有独占访问权限的线程并返回true。即获取锁成功。
setExclusiveOwnerThread(current);
return true;
}
}
//当前线程拥有独占访问权限(即不用排队等操作,已经拿到了锁),直接设置state
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
另外,再解释下 hasQueuedPredecessors 方法,该方法判断当前线程有没有前继线程。
首先我们想一下怎么样能说明该线程没有前继线程?
1. 队列为空,如果队列为空说明压根没有初始化排队队列,谈何排队,那么当前线程必定是即将要获取锁的线程。
2. 队列不为空存在Head节点,但是Head节点的后继节点next为null 或者 后继节点的线程就是当前的线程自己。Head节点的后继节点next为null 的意思就是队列只有Head节点还没有其他节点排队,那么当前线程必定是即将要获取锁的线程;后继节点的线程就是当前的线程自己 的意思就是当前线程是排在Head节点后面的第一个线程,那么理所当然当前线程必定是即将要获取锁的线程。
下面看一下源代码:
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
② 线程排队:addWaiter()
分析addWaiter时,我们先看一张图:
用这张图理解线程排队的场景,Head节点不关联线程,其他节点都关联一个线程。
addWaiter法方主要逻辑分为两大步:
- 第一步:排队队列不为空,将当前节点node的prev指向尾节点tail,然后通过CAS将新节点node设置为尾节点,CAS成功后将原来尾节点的next指向当前节点node。(总的来说就是把新节点node排队为尾节点)
- 第二步:排队队列为空或者第一步CAS失败,调用enq()方法,主要作用是将节点插入队列直至成功,必要时进行初始化。(第二步与第一步差不多,只是确保线程能排队成功,相当于兜底操作)
//线程排队
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
//排队队列不为空,即当前有线程已经在排队了,所以新的线程也需要排队
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
enq()方法主要做两件事情
- 1. 初始化,CAS设置一个头节点。
- 2. 同addWaiter一样,排队操作,区别是会一直尝试直至成功。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
③ 以独占不间断模式获取已在队列中的线程:acquireQueued()
acquireQueued() 主要做两件事情:
- 第一步:判断如果当前线程的prev是head节点并尝试获取锁。当前线程的prev是head节点意思是 当前节点是队列的第一个线程
- 第二步:当前线程不是队列的第一个/获取锁失败,则执行park,最后取消获取锁。
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)) {
//头节点指向node节点,node的thread、prev属性为null,即node成为新的头节点
setHead(node);
//原来的头节点没用了,设置next引用为null等待GC回收
p.next = null; // help GC
failed = false;
return interrupted;
}
//队列中有排队线程或者 if 中获取锁失败,则设置前一个节点的waitStatus=-1
//并且park当前线程,中断线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这里需要说明一下 waitStatus 属性
默认waitStatus = 0,线程排队后节点的 waitStatus = 0
waitStatus = -1 代表后继(下一个)线程需要解停,即需要 unpark 。对应源码中的 SIGNAL 。
waitStatus = 1 代表线程已取消 ,对应源码中的 CANCELLED 。
线程释放锁的时候会将 waitStatus 设置为 0
④ 操作waitStatus状态进行park:shouldParkAfterFailedAcquire
该方法主要是 ① 处理已经取消的节点,② 通过park标记被(或即将)阻塞的节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/* 前继节点还在等待触发,还没当前节点的什么事,所以当前节点可以被park*/
return true;
if (ws > 0) {
/* 前继节点被取消需要从同步队列中删除。
若跳过后的节点还是为CANCELLED ,还需要重复上述步骤
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
标记被(或即将)阻塞的节点
如果waitStatus=0或者PROPAGATE,则将waitStatus设置为 -1
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
2.ReentrantLock#unlock源码分析
释放锁的逻辑比较简单,尝试释放锁成功后就unpark头节点后面的线程。
unpark的操作:从tail节点开始找出队列中排在队列最前面的需要释放锁的线程,进行unpark 。
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//释放锁成功后进行unpark线程操作
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
第一个排队节点为空或者此节点的waitStatus>0,即CANCELLED==1的状态
if (s == null || s.waitStatus > 0) {
s = null;
/* 从tail节点开始找出队列中需要释放锁的线程,进行unpark */
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
3.公平锁、非公平锁的异同
总结
该篇内容主要总结了ReentrantLock的源码内容,进而分析ReentrantLock在获取锁以及释放锁时的原理。以及ReentrantLock公平锁和非公平锁的异同
内容补充中...