JUC ReentrantLock 原理
占用资源成功,没有竞争的情况
-
入口是
java.util.concurrent.locks.ReentrantLock#lock
public void lock() { sync.lock(); }
-
可以知道调用了 sync 属性的 lock 方法
-
sync 属性在创建 ReentrantLock 就确定了是公平锁还是非公平锁
-
这里分析公平锁,所以 sync 会调用 FairSync 的 lock 方法
-
-
java.util.concurrent.locks.ReentrantLock.FairSync#lock
static final class FairSync extends Sync { ... final void lock() { acquire(1); } ... }
- FairSync.lock 调用 acquire 方法,但是 FairSync 没有 acquire,所以要调用继承来的方法
- 直接父类 Sync 也没有 acquire,爷爷类 AQS 有,所以继续看 AQS 的 acquire 做了什么
-
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) { // tryAcquire 是抢占资源(注意这里是取反,当抢占失败才会走后面的逻辑即加入队列,抢占成功就没后面什么事儿了) if (!tryAcquire(arg) && // acquireQueued 是把当前线程加入队列 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
这里要分析两点,1 怎么抢占资源的?2 抢占失败,怎么加入队列的?先看怎么抢占资源,也就是 tryAcquire 逻辑
-
java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquire
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
- tryAcquire 方法直接抛一个异常?是想告诉我们这个方法必须子类实现(面向对象知识:子类重写父类方法,会调用子类的)
- 典型的模板设计模式
- FairSync 继承 Sync 继承 AQS,这里在 AQS 中调用本类方法,但是子类重写了,所以会调用子类重写过的方法,也就是 FairSync.tryAcquire()
-
java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire
// 又回到了 FairSync 中 static final class FairSync extends Sync { ... protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 获取资源状态,volatile 修饰的 state,也是来自于 AQS int c = getState(); // state 为 0 则代表当前资源空闲 if (c == 0) { // 资源空闲为什么不立马占用?还要再执行 hasQueuedPredecessors? if (!hasQueuedPredecessors() && // cas 修改 state compareAndSetState(0, acquires)) { // 设置占用线程为当前线程 setExclusiveOwnerThread(current); return true; } } // state 不为 0 代表当前资源已被占用,当被占用不是直接返回 false,而是判断当前线程是否就是占用资源的线程(重入锁) else if (current == getExclusiveOwnerThread()) { // 重入锁:state = state +1 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } // 返回 false,这时才会执行第 3 步的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法 return false; } ... }
-
重点关注下 state == 0 的情况,当前资源为 0 意味着资源空闲,既然空闲为什么不立马占用资源,还要再判断呢?因为这是公平锁,要看是否需要排队
-
如果是非公平锁,两个地方体现了插队
# java.util.concurrent.locks.ReentrantLock.NonfairSync#lock static final class NonfairSync extends Sync { ... // 一来就 cas,失败再进入 acrquire 方法 final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } ... } # java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 只要资源空闲,再次直接 cas,不会考虑队列时候已经有线程在排队了 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
-
-
tryAcquire 返回 true 说明占用资源成功,流程结束
- 两种情况占用资源成功:1 非公平锁插队成功;2 公平锁此时队列为空,当前线程直接占用资源
- 占用失败的情况还没有分析,占用失败要进行入队、挂起等操作
占用资源失败,加锁失败,处理入队、挂起
- state != 0,并且当前线程不是占有资源的线程
- 公平锁场景,state = 0,但是现在已经有线程在排队
- 非公平锁场景,state = 0,占用失败即插队失败
- cas 是虽然原子的,但是正准备 cas 还没有 cas 的时候,刚好别的线程被唤醒占用了资源
以上三种情况都会导致占用失败,一旦失败就会进行入队、挂起操作,源码如下:
public final void acquire(int arg) {
// tryAcquire 是子类方法,如果返回 false,说明占用资源失败
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
只要占用资源失败就继续判断 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
条件,两个方法分别看
java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
private Node addWaiter(Node mode) {
// 当前线程包装成节点(mode:独占 or 共享)
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;
}
java.util.concurrent.locks.AbstractQueuedSynchronizer#enq
private Node enq(final Node node) {
// 死循环
for (;;) {
Node t = tail;
// 第一次循环,t 应该是 null,表示队列为空,先初始化
if (t == null) {
// 当前节点设置为头节点(头节点只是new了一个对象,对象的属性都是 null!)
if (compareAndSetHead(new Node()))
tail = head; // 尾节点也指向头节点(这一次循环就结束了)
} else {
// 第二次循环,队列已经不为空了,维护链表的指针(就是维护当前节点上一个和后一个节点)
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这是一个死循环,第一次循环 t 肯定是 null,表示队列为空,需要进行初始化,分为两次循环完成
-
第一次循环初始化队列,给队列设置头尾节点
-
第二次循环维护节点的指针,就是把节点维护成一个链表
-
可能不止两次循环,当 cas 失败就会多次循环,反正是个死循环,只有维护好队列才会结束
-
java.util.concurrent.locks.AbstractQueuedSynchronizer#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; // 返回到 acquire 方法中,表示获取了锁,不会中断 return interrupted; } // 执行到这里两种情况,1:当前节点不是排队的第一个;2:当前节点是排队的第一个但抢锁失败 // shouldParkAfterFailedAcquire 计算在获取锁失败后是否需要阻塞线程 // parkAndCheckInterrupt 阻塞并且检查中断状态 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 走到线程挂起就暂停了,不会继续执行,但是循环还未结束!等到当前节点被唤醒 // 唤醒后设置 interrupted = true,并不会返回到 acquire 中,而是继续循环 interrupted = true; } } finally { // 基本不会走到这一步,只是更严谨一点 if (failed) cancelAcquire(node); } }
释放资源,释放锁、唤醒排队的节点
没看到出队操作,后续再研究下
和 synchronized 别搞混了,ReentrantLock 可以重入,但也必须要使用 lock 方法来获取锁,调用了几次 lock 就要调用几次 unlock,意味着重复锁会调用多次 unlock
-
入口是
java.util.concurrent.locks.ReentrantLock#unlock
public void unlock() { // 同加锁一致,sync 是 NonfairSync 或 FairSync // 但是 NonfairSync 和 FairSync 并没有 release 方法,所以会调用父类的方法(直接父类 Sync 也没有,于是调用 AQS 的) sync.release(1); }
-
java.util.concurrent.locks.AbstractQueuedSynchronizer#release
public final boolean release(int arg) { if (tryRelease(arg)) { // 释放锁 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 唤醒下一个节点的线程 return true; } return false; }
- 调用 tryRelease 方法来释放锁(把 state 置为 0 并设置占用线程为 null)
- 释放成功就唤醒排队的线程
-
java.util.concurrent.locks.AbstractQueuedSynchronizer#tryRelease
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
和
tryAcquire
方法一致,也是一个模板方法,调用子类Sync
的tryAcquire
方法 -
java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) { // 获取 state 的值,减去 release(值是1) int c = getState() - releases; // 如果当前线程不是持有资源的线程,这不扯犊子吗,直接抛一个异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 如果 c 是 0 if (c == 0) { free = true; setExclusiveOwnerThread(null); } // cas 设置 state 的值为 0 setState(c); return free; }
-
java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor
private void unparkSuccessor(Node node) { ... // 下一个节点 Node s = node.next; ... // 唤醒下一个节点的线程 if (s != null) LockSupport.unpark(s.thread); }
-
流程结束