背景:一个线程加了两次锁,可重入释放锁的过程
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//1.尝试释放锁
if (tryRelease(arg)) {
//2.唤醒线程流程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
1.tryRelease(arg),尝试释放锁
//ReentrantLock
protected final boolean tryRelease(int releases) {
//1
int c = getState() - releases;
//2
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//3
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//4
setState(c);
return free;
}
第一释放锁
- int c = getState() - releases; 这里getState()为2,因为第一个线程加了锁,releases为1
- Thread.currentThread() != getExclusiveOwnerThread();当前线程不是加锁线程,说明不是当前线程加锁反而来释放锁,如果能释放就是不对的
- 由于现在state减了1还是1,那么这个判断进不去,如果能进去就代表释放锁成功了
- 更改state的值
第二次释放锁
- int c = getState() - releases; 这里getState()为1,因为第一个线程加了锁,releases为1
- 该判断不进入
-
2.唤醒线程流程
public final boolean release(int arg) {
if (tryRelease(arg)) {
//唤醒线程流程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
Node h = head; 搞一个中间变量h 指向head节点
unparkSuccessor(h);
- int ws = node.waitStatus;,head节点的status,此时是个signal
- compareAndSetWaitStatus(node, ws, 0); 就可以执行这个操作,把head节点的status设置成0
- Node s = node.next;拿到第一个等待线程节点,也就是线程2节点
由于s不是null,就能执行LockSupport.unpark(s.thread);,这个方法就传入了s节点中的Thread进行唤醒
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;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
此时,切回到线程2阻塞的代码中
- 唤醒后,线程2就在这个for循环中,此时获取p一定时head,因此就开始执行tryAcquire开始获取锁
为什么从尾部开始唤醒
既然采用了从尾部遍历的逻辑,那么肯定是为了解决可能会出现的问题。而这个问题就在enq(…)方法中:
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;
//set尾部节点
if (compareAndSetTail(t, node)) {//当前节点置为尾部
t.next = node; //前驱节点的next指针指向当前节点
return t;
}
}
}
}
在该段方法中,将当前节点置于尾部使用了CAS来保证线程安全,但是请注意:在if语句块中的代码并没有使用任何手段来保证线程安全!
也就是说,在高并发情况下,可能会出现这种情况:
线程A通过CAS进入if语句块之后,发生上下文切换,此时线程B同样执行了该方法,并且执行完毕。然后线程C调用了unparkSuccessor方法。
- 线程A执行CAS将当前节点置为尾部:
- 原本线程A要执行t.next = node;将node2的next设置为node3,但是,此时发生上下文切换,时间片交由线程B,也就是说,此时node2的next还是null
- 线程B执行enq逻辑,最终CLH队列如图所示:
- 此时发生上下文切换,时间片交由线程C,线程C调用了unparkSuccessor方法,假如是从头到尾的遍历形式,在node2就会发现,next指针为null,似乎没有后续节点了。
其最根本的原因在于:
node.prev = t;先于CAS执行,也就是说,你在将当前节点置为尾部之前就已经把前驱节点赋值了,自然不会出现prev=null的情况