一、支持公平锁。
在ReentrantLock中,有个内部类
Sync
继承了AbstractQueuedSynchronizer类,
Sync在ReentrantLock类中有两个子类FairSync
和NonFairSync
,也就是公平锁与非公平锁。
公平锁会按照AQS队列中的节点顺序进行加锁,而非公平锁不用管队列中是否有其它节点,就可以尝试去获取锁,获取失败后再加入到队列中
公平锁与非公平锁的加锁方式的区别在源码中是怎么实现的?
首先看lock()方法的区别:
- 非公平锁
- 公平锁:
两段代码的主要区别是非公平锁会直接尝试获取锁,没有获取到锁再去执行acquire()方法,而公平锁直接执行acquire()
下面我们来看acquire()
acquire()是Sync从AbstractQueuedSynchronizer中继承的
并且没有重写,所以公平锁与非公平锁的acquire()方法是一样的
接着,我们来分析上图中标记出来的三个方法
- 首先执行了
tryAcquire
方法,该方法的作用是尝试获取锁,在公平锁与非公平锁中有不同的实现,两者之间最大的区别就表现在这里
- 非公平锁
- 公平锁
两段代码的逻辑基本上一致,不同在于tryAcquire()比nonfairTryAcquire()增加了
hasQueuedPredecessors
的逻辑判断,根据方法名就可知道该方法是用来判断同步队列中是否有前驱节点的,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,才有做后面的逻辑判断的必要性。
以下方法都是在AQS中实现的,在公平锁和非公平锁中没有区别
2、如果tryAcquire没有获取到锁,则继续执行
addWaiter()
方法,该方法会把当前线程封装成Node节点放入到队列,并返回该节点
3、加入队列之后,执行
acquireQueued()
方法
说明:该节点没有获取到锁,放入到队列后,开始自旋;
自旋过程:首先获取该节点的前驱节点,判断是否为头结点,如果是,则再次尝试去获取锁,获取成功,将该节点设置为头节点;获取失败,继续执行shouldParkAfterFailedAcquire()
,在该方法中为其前驱节点增加一个唤醒该节点的任务,首次设置会返回false,因为是短路&&,本次自旋不会执行parkAndCheckInterrupt()
方法;但是在下一次自旋时,如果还是没有获取到锁,那么就会执行parkAndCheckInterrupt()挂起该线程。这两个方法的具体实现不再详细说明,有兴趣的话可以自行查看源码 😉
二、等待可中断
首先来看下不可中断模式,也就是我们通过lock方法获取锁的情况下
static final class NonfairSync extends Sync {
// ...
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;
failed = false;
// 还是需要获得锁后, 才能返回打断状态
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
// 如果是因为 interrupt 被唤醒, 返回打断状态为 true
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
// 如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// interrupted 会清除打断标记
return Thread.interrupted();
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
// 如果打断状态为 true
selfInterrupt();
}
}
static void selfInterrupt() {
// 重新产生一次中断
Thread.currentThread().interrupt();
}
}
说明:线程没有获取到锁,会执行parkAndCheckInterrupt()进行等待,我们打断该等待,线程继续执行,设置interrupted = true,再次进行自旋,如果没有成功获取到锁,会再次执行parkAndCheckInterrupt()方法等待。如果这次获取到了锁,
会将之前设置的interrupted = true作为返回值,返回到acquire()方法,这时acquire()方法中的所有判断条件都满足,执行selfInterrupt()方法,使该线程再次中断。
等待可中断:通过lockInterruptibly()方法获取锁
static final class NonfairSync extends Sync {
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果没有获得到锁, 进入 ㈠
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
// ㈠ 可打断的获取锁流程
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
// 在 park 过程中如果被 interrupt 会进入此
// 这时候抛出异常, 而不会再次进入 for (;;)
throw new InterruptedException();
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}
说明: 线程没有获取到锁,会执行parkAndCheckInterrupt()进行等待,我们打断该等待,这时候直接抛出异常, 而不会再次进入 for ( ;; )
三、多条件状态绑定
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject。ConditionObject是AQS的内部类,同样是基于AQS中的Node类维护等待队列
await 流程
private Node addConditionWaiter() {
Node t = lastWaiter;
// 所有已取消的 Node 从队列链表删除.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建一个关联当前线程的新 Node, 添加至队列尾部
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程
创建新的 Node 状态为 -2(Node.CONDITION关),联 Thread-0,加入等待队列尾部
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁(因为可能发生了锁重入,所以需要调用fullyRelease释放掉所有锁)
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
park 阻塞 Thread-0
signal 流程
假设 Thread-1 要来唤醒 Thread-0
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1
说明:执行doSignal 方法不意味着线程可以立刻获取到锁,可以理解为把该节点从条件队列中取出,再放入到AQS队列中,等待前驱节点的唤醒
学习视频:黑马程序员并发编程