ReenterLock源码解析
本文只解析加锁解锁部分,忽略Condition部分。以公平锁为例。
锁的基本思想
锁的目的
要让同一时间只能有一个线程占有共享的资源。
锁的实现思路
如何达到这个目的呢?
需要考虑1.用什么作为锁,2.如何获得锁(同时考虑:如何阻塞其他线程),3.如何释放锁(同时考虑:如何唤醒其他线程)?
ReentreLock实现
顺着这个思路,带着三个问题,来看ReenterLock的实现:
1.state作为锁;
2.当state=1时表示获得锁,=0表示锁空闲
此时其他线程如何阻塞?
并发场景按照线程之间的执行程度分为两种情形:
用电影票售票场景举例,售票窗口是共享资源,那么:
2.1.交替执行。(CAS尝试获取锁)
假设一个非热卖点的售票时间,人不算多。
有两个人A,B。A先到,正在办理售票(占用资源),后一个人也在赶来,当B赶到,A已经受理结束了。虽然并发,但是线程之间请求资源的时间点有先后顺序,阻塞时间忽略不计。
2.2.争抢执行。(队列)
假设一个假期恰好有个火热的电影上映,人特别多。
有五六个人同时到达办理窗口,他们谁先谁后,怎么知道呢,售票窗口小姐姐喊道“排队!”,这时候阻塞明显,好吧只能通过排队去办理了。
所以:
尝试获取锁的方式有两种:CAS,排队。
3.双向链表的队列存储需要唤醒的线程
如何唤醒排队中的人呢?需要队头办好业务的节点去唤醒下一个节点上的线程,而线程中的节点有可能会取消掉了,需要调整队伍。而列表中要知道节点的上一个节点,单向列表需要记住上一节点的位置下标,重头遍历才能找到,而双向列表只需访问上一指针。所以使用了双向链表,提高查询效率。
ReenterLock的内存分配
1.初始化
2.队列维护空间
head和tail的作用是:
head:用来指向队头第一个办理业务的人;
tail:用来记录排在队尾的人,再有人来排队需要排在队尾。
需要排队:
由于排队中的成员都是被阻塞的,睡眠的,不运行的,不能自己察觉到办理窗口是否可用,需要等待正在办理业务的人来唤醒队列中的人。正在受理的人必须是队列中的成员,他才知道要叫醒谁,也就是说他是一个节点他才知道要叫醒谁。
因此:队列在初始化时会创建一个正在持有锁的线程作为对头。
3.队列初始化
假设锁被线程t1占用,即state=1,队列初始化内存空间如下图所示:
4.入队
线程进入阻塞队列,就是双向链表添加节点的操作一样:
假设现在持有锁的线程是t1,排在后面的是t2
正在有个线程t3加入,加入步骤是:
t3将作为队尾,但是先要把t3链接到当前队列:tail.next=t3;
t3.prev=tail;
t3作为队尾:tail=t3.
此时空间结构如下图所示:
来看源码
lock()
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如下我将三个方法的流程图画下来
tryAcquire,addWaiter,acquireQueued
tryAcquire/尝试获得锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
addWaiter/添加队列成员
private Node addWaiter(Node mode) {
//新增一个节点指向当前线程
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//队列是否为空
if (pred != null) {
//不为空则直接插入队尾
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//队列为空则设置队头(添加一个thread=null作为持有线程的节点),再将当前线程的节点放在队尾
enq(node);
return node;
}
acquireQueued/入队后再尝试获取锁
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)) {
//拿到锁则更改当前节点为头部节点
setHead(node);
p.next = null; // help GC
failed = false;
//获取成功则加锁流程结束
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//waitStatus标志已经前一节点已经检查过一次了,前一节点状态是等待状态,不用再往前问也不尝试拿锁,可直接park了。
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
//跳过状态为cancel的节点,往前一个节点连接
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
此方法目的是再试拿一下锁,拿不到则自己阻塞。
sinal状态的作用:标志已经前一节点已经检查过一次了,前一节点状态是等待状态,不用再往前问也不尝试拿锁,可直接park了。
橙色线头为死循环,只有获取到锁,或者调用park阻塞,才跳出循环
unlock()
释放锁需要做的事情:
1.设置state=0;(但是有重入的情况,这里跟加锁对应,加锁是state+1,那么这里是state-1)
2.设置持有锁的线程变量=null;
3.唤醒下一个节点。(这里有个要注意的地方,当前节点的下一个节点如果为空,是从尾部向前查询,拿到最邻近自己的睡眠节点,那么问题来了,为什么是尾部查询。原因我已经注释在代码上了)
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;
//2.设置持有锁的线程变量=null;
setExclusiveOwnerThread(null);
}
//1.设置state=0;
setState(c);
return free;
}
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;
if (ws < 0)
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;
//*** 【原因】:执行到这里,如果其他线程获取到当前节点,并在当前节点后加了节点node2,那么继续向后查询就查不到node2了,那么久没办法唤醒node2,要等到下一个线程持有再释放锁时再来唤醒node2,这无非降低了效率。
//从节点尾部向前查,查最靠近当前节点的等待状态的节点。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//3.唤醒下一个节点。
LockSupport.unpark(s.thread);
}
总的一句话,ReenterLock是基于volatile+CAS+park+自旋 实现的