//一个尝试插队的过程
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取state值
int c = getState();
//比较锁的状态是否为 0,如果是0,当前没有任何一个线程获取锁
if (c == 0) {
//则尝试去原子抢占这个锁(设置状态为1,然后把当前线程设置成独占线程)
if (compareAndSetState(0, acquires)) {
// 设置成功标识独占锁
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前锁的状态不是0 state!=0,就去比较当前线程和占用锁的线程是不是一个线程
else if (current == getExclusiveOwnerThread()) {
//如果是,增加状态变量的值,从这里看出可重入锁之所以可重入,就是同一个线程可以反复使用它占用的锁
int nextc = c + acquires;
//重入次数太多,大过Integer.MAX
if (nextc < 0) // overflow
throw new Error(“Maximum lock count exceeded”);
setState(nextc);
return true;
}
//如果以上两种情况都不通过,则返回失败false
return false;
}
- tryAcquire() 一旦返回 false,就会则进入 acquireQueued() 流程,也就是基于CLH队列的抢占模式,在CLH锁队列尾部增加一个等待节点,这个节点保存了当前线程,通过调用 addWaiter() 实现,这里需要考虑初始化的情况,在第一个等待节点进入的时候,需要初始化一个头节点然后把当前节点加入到尾部,后续则直接在尾部加入节点。
代码如下:
//AbstractQueuedSynchronizer.addWaiter()
private Node addWaiter(Node mode) {
// 初始化一个节点,用于保存当前线程
Node node = new Node(Thread.currentThread(), mode);
// 当CLH队列不为空的视乎,直接在队列尾部插入一个节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
//如果pred还是尾部(即没有被其他线程更新),则将尾部更新为node节点(即当前线程快速设置成了队尾)
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 当CLH队列为空的时候,调用enq方法初始化队列
enq(node);
return node;
}
private Node enq(final Node node) {
//在一个循环里不停的尝试将node节点插入到队尾里
for (;😉 {
Node t = tail;
if (t == null) { // 初始化节点,头尾都指向一个空节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 将节点增加到CLH队列后,进入 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)) {// 通过tryAcquire获得锁,如果获取到锁,说明头节点已经释放了锁
setHead(node);//将当前节点设置成头节点
p.next = null; // help GC//将上一个节点的next变量被设置为null,在下次GC的时候会清理掉
failed = false;//将failed标记设置成false
return interrupted;
}
//中断
if (shouldParkAfterFailedAcquire(p, node) && // 是否需要阻塞
parkAndCheckInterrupt())// 阻塞,返回线程是否被中断
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 如果尝试获取锁失败,就会进入 shouldParkAfterFailedAcquire() 方法,会判断当前线程是否阻塞
/**
-
确保当前结点的前驱结点的状态为SIGNAL
-
SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程
-
只有确保能够被唤醒,当前线程才能放心的阻塞。
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前驱节点状态为SIGNAL
//表明当前线程需要阻塞,因为前置节点承诺执行完之后会通知唤醒当前节点
return true;
if (ws > 0) {//ws > 0 代表前驱节点取消了
do {
node.prev = pred = pred.prev;//不断的把前驱取消了的节点移除队列
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//初始化状态,将前驱节点的状态设置成SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
- 当进入阻塞阶段,会进入 parkAndCheckInterrupt() 方法,则会调用 LockSupport.park(this) 将当前线程挂起。代码如下:
// 从方法名可以看出这个方法做了两件事
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//挂起当前的线程
// 如果当前线程已经被中断了,返回true,否则返回false
// 有可能在挂起阶段被中断了
return Thread.interrupted();
}
4.2 非公平锁 NonfairSync.unlock()
2.1 unlock()方法的示意图
2.1 unlock()方法详解
-
调用 unlock() 方法,其实是直接调用 AbstractQueuedSynchronizer.release() 操作。
-
进入 release() 方法,内部先尝试 tryRelease() 操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。
-
如果 tryRelease 成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。
-
一旦下一个节点的线程被唤醒,被唤醒的线程就会进入 acquireQueued() 代码流程中,去获取锁。
代码如下:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//尝试在当前锁的锁定计数(state)值上减1,
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)//waitStatus!=0表明或者处于CANCEL状态,或者是SIGNAL表示下一个线程在等待其唤醒。也就是说waitStatus不为零表示它的后继在等待唤醒。
unparkSuccessor(h);
//成功返回true
return true;
}
//否则返回false
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//如果waitStatus < 0 则将当前节点清零
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//若后续节点为空或已被cancel,则从尾部开始找到队列中第一个waitStatus<=0,即未被cancel的节点
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);
}
当然在 release() 方法中不仅仅只是将 state - 1 这么简单,-1 之后还需要进行一番处理,如果 -1 之后的 新state = 0 ,则表示当前锁已经被线程释放了,同时会唤醒线程等待队列中的下一个线程。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//判断是否为当前线程在调用,不是抛出IllegalMonitorStateException异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//c == 0,释放该锁,同时将当前所持有线程设置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//设置state
setState©;
return free;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从后往前找到离head最近,而且waitStatus <= 0 的节点
// 其实在ReentrantLock中,waitStatus应该只能为0和-1,需要唤醒的都是-1(Node.SIGNAL)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);// 唤醒挂起线程
}
重点:unlock最好放在finally中,因为如果没有使用finally来释放Lock,那么相当于启动了一个定时炸弹,如果发生错误,我们很难追踪到最初发生错误的位置,因为没有记录应该释放锁的位置和时间,这也就是 ReentrantLock 不能完全替代 synchronized 的原因,因为当程序执行控制离开被保护的代码块时,不会自动清除锁。
4.3 公平锁 FairSync
FairSync相对来说就简单很多,只有重写的两个方法跟NonfairSync不同
final void lock() {
acquire(1);
}
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;
}
-
锁的公平性是相对于获取锁的顺序而言的。
-
如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO,线程获取锁的顺序和调用lock的顺序一样,能够保证老的线程排队使用锁,新线程仍然排队使用锁。
最后
由于篇幅限制,小编在此截出几张知识讲解的图解
;
setState(nextc);
return true;
}
return false;
}
-
锁的公平性是相对于获取锁的顺序而言的。
-
如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO,线程获取锁的顺序和调用lock的顺序一样,能够保证老的线程排队使用锁,新线程仍然排队使用锁。
最后
由于篇幅限制,小编在此截出几张知识讲解的图解
[外链图片转存中…(img-p0XgKYUe-1720117765771)]
[外链图片转存中…(img-JOgQRNhk-1720117765772)]
[外链图片转存中…(img-zopKYRPx-1720117765772)]
[外链图片转存中…(img-qurppBHf-1720117765773)]
[外链图片转存中…(img-bHRSqWe6-1720117765774)]