本篇的内容介绍
本篇文章会详细分析reentrantlock的工作原理,会分为三部分讲解,第一部分是 reentrantlock 大体上的脉络,第二部分是分析公平锁的实现,第三部分再简单对比非公平锁的实现
第一部分 reentrantlock 的一个大概设计
我们简单的看一下reentrantlock 的一个大的设计是怎么样的,我们简单用一个伪代码表示
public class ReentrantLock {
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
class Sync extends AbstractQueuedSynchronizer{
abstract void lock();
// 下面是其他一些公共实现
}
class NonfairSync extends Sync{
void lock() {
//实现
}
}
class FairSync extends Sync{
void lock() {
//实现
}
}
public void lock() {
sync.lock();
}
}
总结:
1 从内部类的结构可以比较清晰的看出来,ReentrantLock 分为 NonfairSync(非公平锁) 和 FairSync(公平锁), 都是依赖与 AQS 同步器实现的。
2 默认为非公平锁
3 初始化后 ReentrantLock 持有Sync实例,ReentrantLock 大部分实现,是调用Sync方法实现的
第二部分 公平锁的实现原理
一 我们先看看公平锁,加锁过程
步骤总结:
1 先尝试获取锁,如果获取锁成功,则加锁成功
2 如果获取锁失败,则走AQS 的入队流程,下面详细会讲
加锁过程源码分析:
公平的 lock实现
final void lock() {
acquire(1);
}
lock 会调用 AQS(AbstractQueuedSynchronizer) 的 acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁,这个一般由父类提供
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 尝试获取锁失败,会进入AQS队列中参与排队
selfInterrupt();
}
总结:
1 该方法是先尝试获取锁,如果成功则加锁成功
2 如果失败则 调用 addWaiter 方法入队,然后调用 acquireQueued 方法 判断是否需要让出CPU,下面我们一步步的看
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()) { //锁重入,直接返回true
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
总结:
1 getState()==0, 先判断锁是否已经被释放了
2 如果锁已经被释放了,判断是否需要排队,如果需要排队,则返回false,否则尝试加锁
3 如果锁还未被释放,判断当前线程是否为当前拥有锁的线程,如果是,则返回true,说明是锁重入了
4 其他情况即是锁即没被释放,又不是锁重入,则返回false,尝试加锁不成功
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
总结:
1 这是判断是否需要排队的方法,如果要则返回true,否则返回false上面代码,可以分为几种情况讨论,我们使用如下代码表示
/*
什么情况下 AQS 队列 head != tail
第一种是 队列还没有初始化的时候, 此时 head 和 tail 都为 null。
第二种是 已经初始化了队列,但是此时队列没有任何等待线程
*/
if((h!=t) == false){
return false;
}
/*
要使 ((s = h.next) == null || s.thread != Thread.currentThread()) ====> 返回false 那么
(s = h.next) == null 和 s.thread != Thread.currentThread() 都需要同时返回 false
什么情况下 (s = h.next) == null 且 s.thread != Thread.currentThread() 都为 false 呢?
1 (s = h.next) == null 为false ,就是队列不为空,且至少又一个元素
2 s.thread != Thread.currentThread() ===为false , 当前线程就是第一个等待元素,意思就是该线程是队列中的除head的第一个元素,不需要排队
总结:如果队列至少有一个元素,且当前线程就是第一个等待元素,则不需要排队
什么情况下下面条件会成立呢?
如果是该元素就是第一个等待元素,那么就是以下场景===》尝试获取锁失败时,调用addWaiter加入队列,如果是队列中的除head的第一个元素,
那么acquireQueued 会再次调用 tryAcquire 获取锁,此时 该元素就是第一个等待元素 这个条件就会满足,此处可以看完下面再倒回来看
*/
if((h!=t) == true && ((s = h.next) == null || s.thread != Thread.currentThread()) ==false){
return false;
}
/*
要让整个表达式为 true,必须是
h!=t 和 ((s = h.next) == null || s.thread != Thread.currentThread()) 同时为 true
((s = h.next) == null || s.thread != Thread.currentThread()) 为true 分为两种情况,
(s = h.next) == null 为 true 或 s.thread != Thread.currentThread() 为true
如果 h!=t 为true 那么 (s = h.next) == null 必然为false, 那么使条件成立,则
h!=t 为 true
s.thread != Thread.currentThread() 为 true
(s = h.next) == null 为 false
这种情况也就是说,队列不为空,且当前 线程,不是第一个非head 元素。
*/
if(h!=t == true && ((s = h.next) == null || s.thread != Thread.currentThread()) ==true){
return true;
}
总结:
1 如果队列没有被初始化或者队列没有任何元素,则不需要排队
2 如果队列至少有一个元素,且当前线程就是第一个等待元素,则不需要排队,这种场景一般发生在,刚入队完,判断是否需要让出CPU的时候发生
3 队列不为空,且当前 线程,不是第一个非head 元素,则需要排队
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 如果队列不为空,先尝试一次入队,如果入队不成功,会进入 enq 方法。
// 如果有竞争的情况下,A,B线程 只能有一个成功,所以会有入队失败的情况
Node pred = tail;
if (pred != null) {
// 下面就是维护队列的元素关系,这里就不细说了
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
总结:
1 这是队列节点入队操作,如果队列不为空,则会尝试直接做入队操作
2 如果队列为空或者入队失败,则进入 enq 方法进行操作
private Node enq(final 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;
}
}
}
}
总结:
情景一:A,B两个线程竞争入队,且队列没有被初始化。
我们一起来缕一缕这个逻辑:
1 第一次循环,因为队列为空,A,B线程同时进入 t == null 这个条件,此时无论谁 compareAndSetHead 成功,队列都会被初始化,进入第二次循环.
2 第二次循环,因为队列已经被初始化了,所以会进入条件2,如果A 成功入队,即 compareAndSetTail 成功,则A直接返回。在竞争的情况下,A成功,则B就入队失败,B会进入第三次循环。
3 第三次循环,没有线程与B竞争,则B线程入队成功。
final boolean acquireQueued(final Node node, int arg) { // 判断是否需要让出CPU
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 如果该元素是非head得首个元素,那么可以有资格尝试 获取锁
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && // 判断是否需要让出cpu
parkAndCheckInterrupt()) // 如果需要则调用 LockSupport.park(this) 让出cpu
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
总结:
1 判断是否需要让出CPU,如果当前线程是队列的非head得首个元素,那么可以有资格尝试 获取锁
2 否则调用 shouldParkAfterFailedAcquire 方法判断是否需要让出CPU,我们结合 shouldParkAfterFailedAcquire 一起看
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //如果前一个结点时 SIGNAL 状态,该节点则可以让出CPU
return true;
if (ws > 0) {
// 如果前一个节点的状态为 cancelled,则跳过前一个节点,将前一个节点设置为前面
//最靠近当前节点的 signal状态的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前一个节点的等待状态为 null 或 PROPAGATE,此时将前一个节点的状态设置为 SIGNAL
// 进入第二遍循环
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
总结:
1 如果前一个节点是 SIGNAL,则可以让出CPU
这里有一个场景,因为独占锁刚加入节点,waitStatus都是null
所以通常进入 acquireQueued,会进行两次循环。
1 第一次 循环 将 前一个节点 的 waitStatus 设置为 SIGNAL
2 第二次 循环 shouldParkAfterFailedAcquire 就可以返回true了,因为第一次循环已经将前一个节点设置为 SIGNAL
换句话说,在acquireQueued里,如果该元素是非head得首个元素,那么可以有两次资格尝试 获取锁
公平锁,加锁过程就说到这里,下面我们来看看公平锁,解锁过程
二 我们再看看公平锁,解锁过程
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁,一般由父类提供
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //重队列中唤醒符合条件的线程,我们下面来分析
return true;
}
return false;
}
总结:
1 tryRelease 是产生释放锁,这一般由父类提供
2 释放锁成功后,会唤醒队列中一个符合条件的线程
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //state大于一的情况下,为锁重入了,所以state减至为0,则锁释放
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
总结:
1 state 减至0则 释放锁
2 释放成功,设置占有锁的线程
private void unparkSuccessor(Node node) { // 唤醒后继线程
/*
将node 的 waitStatus设置为0,这样再次进入 acquireQueued 可以多一次获取锁的机会,可以回头看看acquireQueued的实现
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
如果node.next 不为null 且 waitStatus 不为CANCELLED 状态,则唤醒线程,
否则,从tail 往前开始找到一个符合条件的节点
*/
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); // 唤醒线程
}
总结:
1 将node 的 waitStatus重置为0。这样非head的头节点,就会有再多一次的获取锁的机会
2 获取头节点的下一个节点,如果不满足条件(为空或则waitStatus为CANCELLED 状态),则从tail 往前开始找到一个符合条件的节点
3 如果找到符合条件的节点,则唤醒线程
第二部分,简单对比非公平锁的实现
我们来看个图
final void lock() {
if (compareAndSetState(0, 1)) // 先尝试获取锁
setExclusiveOwnerThread(Thread.currentThread()); // 设置该线程为当前拥有锁的线程
else
acquire(1); // 如果获取锁失败,走AQS的实现
}
总结:
非公平锁的lock实现
1 先cas尝试获取一次锁
2 获取锁失败则走AQS实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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
1 如果当前锁已经释放,不判断是否需要排队,直接cas 获取一次锁
2 其他跟公平锁差不多
这里总结一个非公平锁和公平锁的一些区别:
1 非公平锁在 lock 的时候,直接cas 一次尝试获取锁,公平锁则 直接走 AQS的aquire
2 非公平锁TryAcquire 的时候,直接cas 一次尝试获取锁,公平锁则判断是否需要排队,不需要排队才尝试获取锁
参考资源:
https://blog.csdn.net/fuyuwei2015/article/details/83719444
https://blog.csdn.net/java_lyvee/article/details/98966684
https://www.cnblogs.com/micrari/p/6937995.html