概述
这篇文章算是前两篇文章的补充,这里比较集中的讲解非公平锁、公平锁以及各种锁的超时等待的逻辑。
1.公平锁与非公平锁
1.1.ReentrantLock
非公平模式,其实就是无论等待队列中是否还有其他排队的节点,都在 lock 的时候立刻尝试获取一次锁,这里我们拿ReentrantLock举个简单的例子:
// 非公平锁的 lock 方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 公平锁的 lock 方法
final void lock() {
acquire(1);
}
一目了然。
1.2.ReentrantReadWriteLock
在 ReentrantReadWriteLock 中,由于区分读写锁,因此它稍微绕了一层。在 Sync 内部类中,提供了两个抽象方法:
/**
* Returns true if the current thread, when trying to acquire
* the read lock, and otherwise eligible to do so, should block
* because of policy for overtaking other waiting threads.
*/
abstract boolean readerShouldBlock();
/**
* Returns true if the current thread, when trying to acquire
* the write lock, and otherwise eligible to do so, should block
* because of policy for overtaking other waiting threads.
*/
abstract boolean writerShouldBlock();
注释也说的很明白了,那就是获取锁的时候,判断一下到底能不能真正的获取锁。 这是公平锁的实现:
// 不论读写锁,只要队列里有排队的线程,全部都乖乖去排队
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
这是非公平锁的实现:
// 不管队列里有没有排队的线程都试一下
final boolean writerShouldBlock() {
return false;// writers can always barge
}
// 如果排队的线程抢的是写锁,那就不插队,否则就插队试试
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null && // 有头结点,说明有线程正在持有锁
(s = h.next) != null && // 头结点有后继节点,说明有节点在等待锁
!s.isShared() && // 是否共享模式,即这个等待锁的线程要的是写锁
s.thread != null;
}
// Node 内部类中的方法,此处可见 addWaiter 时指定的模式派上了用场
final boolean isShared() {
return nextWaiter == SHARED;
}
非公平锁也很简单,写锁无论如何都会去抢一下,而读锁需要确认在排队的线程是不是要抢写锁,如果是那就不插队了,否则直接插队。
2.ReentrantLock的超时机制
我们会注意到 Lock 接口提供了两种 tryLock 方法:
boolean tryLock(long time, TimeUnit unit);:等待指定时间,在指定时间内拿不到锁就失败;boolean tryLock();:只尝试一次,没拿到锁立刻失败;
在阅读过前两章后,我们很容易猜到它们各种的实现逻辑。
2.1.不指定超时时间
在 ReentrantLock 中,tryLock 最终会调用到 Sync 内部类的 nonfairTryAcquire 方法:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
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 方法:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
2.2.指定超时时间
当指定超时时间时,会先统一的把时间转为纳秒,然后调用 AQS 的 tryAcquireNanos 方法:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
一如既往地先用 tryAcquire 抢一下锁,失败才真正的进入 doAcquireNanos 方法中:
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
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 true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold) // spinForTimeoutThreshold 是个内部静态常量,它表示 AQS 中默认的自旋超时时间,默认为 1000L
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
doAcquireNanos 方法与正常加锁的时在队列中等锁的 acquireQueued 方法逻辑基本一致,但是多了时间检测:
- 根据当前时间,以及指定的等待时间,计算超时的时间点
deadline; - 进入循环,尝试 CAS 获取锁, 当获取锁失败时:
- 计算当前时间与
deadline之间的时间差nanosTimeout,如果已经小于0,即当前已经超时了,那么直接中断循环加锁失败; - 如果时间差
nanosTimeout大于1000纳秒,那么就调用LockSupport.parkNanos挂起线程。 直到被前驱节点唤醒,或者到了deadline后再起来抢锁; - 如果时间差
nanosTimeout小于1000纳秒,说明距离deadline没剩多少时间了,直接继续抢锁,没必要再挂起了; - 如果线程中断了,就直接抛异常;
- 计算当前时间与
- 如果在上述循环中抛出异常(比如调用
tryAcquire时),那就调用cancelAcquire移除当前节点并唤醒后继节点,否则就正常的退出;
我们需要关注在这个方法中,线程竞争锁的几个特殊行为:
- 首先,进来立刻抢一次锁,抢不到就进等待队列;
- 在等待队列里等锁,总是默认等到
deadline,不过在这个过程中有机会被前驱节点在deadline前唤醒,因此有机会多抢几次; - 如果醒了以后离
deadline不到 1000 纳秒了,那直接不需要挂起,反复疯狂 cas 直到超时;
3.ReentrantReadWriteLock的超时机制
读写锁在不指定超时时间时逻辑基本差不多,因此我们重点关注读写锁在指定超时时间时的实现。
3.1.读锁
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
独占锁调用的是 doAcquireNanos ,而非独占锁调用的是 doAcquireSharedNanos :
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null;// help GC
failed = false;
return true;
}
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout >spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
跟 doAcquireNanos 的代码不能说一模一样,只能说完全一致,唯一改变的地方只有:
- 加锁调用的是
tryAcquireShared而不是tryAcquire; - 成功加锁后,调用的是
setHeadAndPropagate而不是setHead;
3.2.写锁
写锁这边与 ReentrantLock 完全一致,都是通过 AQS 的 tryAcquireNanos 完成:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
总结
公平锁和非公平锁
- 对于
ReentrantLock,公平锁和非公平锁的主要区别在于,在调用Lock之前会多做一次判断。如果是公平锁,在有排队线程时会直接去排队,而非公平锁无论如何都会试着抢一次锁; - 对于
ReentrantReadWriteLock,它的公平锁的逻辑与ReentrantLock一致,即无论读写锁,在当前队列有排队线程的情况下都会直接去排队。但是对于非公平锁中的读锁,只有等待队列的前驱节点为共享节点时才会尝试抢占锁;
等待超时
不管是 ReentrantLock 还是 ReentrantReadWriteLock,当调用 tryLock 并且指定超时时间时,都会将超时时间转为纳秒并且调用 AQS 的 tryAcquireNano/tryAcquireSharedNano 方法,在这些方法中,线程将会:
- 先根据指定的操作时间计算出真正的超时时间点
deadline; - 循环的尝试通过 cas 加锁:
- 如果加锁成功,那么直接返回;
- 如果加锁失败,且距离
deadline大于 1000 纳秒,就调用LockSupport.parkNano让当前线程挂起到deadline,在这个过程中,如果被前驱节点唤醒,那就继续重试这个步骤,否则就真等到deadline后才会被唤醒; - 如果加锁失败,且距离
deadline小于 1000 纳秒,那么当前线程无需挂起,直接循环尝试获取锁,直到获取锁或者超时为止;