ReectrantLock 和 AQS 是什么关系
ReectrantLock 是在 AQS 外面包了一层,ReectrantLock 所有的加锁、解锁、打断、取消等等,底层都是交给 AQS完成的。
ReectrantLock 标准使用方式
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
// 加锁
lock.lock(); // block until condition holds
try {
// 被加锁的代码块
// ... method body
} finally {
// 解锁
lock.unlock()
}
}
解锁过程
public void unlock() {
// 调用非公平锁的释放锁方法。
sync.release(1);
}
public final boolean release(int arg) {
// 释放锁,
if (tryRelease(arg)) {
// 拿头结点。
Node h = head;
// t头结点不是空,而且头结点的 waitStatus 此时是 -1。
if (h != null && h.waitStatus != 0)
// 唤醒离头结点最近的没有被取消的节点。
// 这里是传了头结点进去。
unparkSuccessor(h);
return true;
}
return false;
}
释放锁的细节:
protected final boolean tryRelease(int releases) {
// 这跟锁重入有关系,当 c == 0 时,才能释放成功。
// 比如,T0 加锁四次,getState() == 4,释放三次 releases ==3
// c=4-3, T0 当然不能释放成功,还有一层锁呢。
int c = getState() - releases;
// 如果当前线程不是锁的持有者,抛异常。
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 当 c==0 时,锁才能释放成功。
if (c == 0) {
free = true;
// 将同步器中锁的持有者置为 null。
setExclusiveOwnerThread(null);
}
// CAS 修改 state = 0。
setState(c);
// 返回
return free;
}
唤醒离头结点最近的没有被取消的节点,在头结点后面 可能有节点(线程)已经被取消了,那就将这些被取消的线程从队列中移除,这个样的话,第一个没有被取消的节点就放在了头结点后面,也就是队列中的第二个节点的位置。
// node == head
private void unparkSuccessor(Node node) {
// ws == -1
int ws = node.waitStatus;
if (ws < 0)
// 设置队头节点的 waitStatus == 0
compareAndSetWaitStatus(node, ws, 0);
// s 先指向队列中的第二个节点。
Node s = node.next;
// 如果第二个节点是空,或者被取消了。
if (s == null || s.waitStatus > 0) {
s = null;
// t 从队尾开始,朝着队头的方向,扫一遍队列中的所有节点。
for (Node t = tail; t != null && t != node; t = t.prev)
// s 指向离队头最近的且没有被取消的线程节点。
if (t.waitStatus <= 0)
s = t;
}
// unpark s 指向的节点中的线程。
if (s != null)
LockSupport.unpark(s.thread);
}
唤醒 node 后,node还要继续去竞争锁。
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;
}
// 竞争失败,继续追加到队列里 park。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 从这里唤醒的。
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
被唤醒变了怎么还要去竞争锁,而且还有失败的可能性?
因为这是非公平锁,既有被唤醒的,可能同时还有刚产生的线程,两者会竞争锁。
总结
释放锁过程做三件事情:
- 释放锁
- 唤醒离头结点最近的第一个没有被取消的 node。
- 被唤醒的 node 再去竞争锁,成功,node出队执行逻辑代码,失败,继续追加到队列里 park。