在上两篇关于《高并发之JUC——AQS源码深度分析(一)》、《高并发之JUC——AQS源码深度分析,有你不得而知的条件等待队列(二)》文中介绍了Doug Lea设计的JUC高并发工具类AQS的设计思想及源码分析,同时也说明了AQS是整个JUC包其他工具类的基石。本文将介绍JUC工具包下面的首个实现类ReentrantLock,即公平锁和非公平锁。
public static void main(String[] args) throws Exception{
// 重入锁,显示锁
ReentrantLock reentrantLock = new ReentrantLock();
// 条件队列。可调用signal方法来唤醒其他线程,相当于notify,可调用await方法阻塞当前线程,相当于wait。
Condition condition = reentrantLock.newCondition();
new Thread(()-> {
// 加锁
reentrantLock.lock();
System.out.println(Thread.currentThread().getId());
try {
Thread.sleep(1000);
// 唤醒其他线程,其他线程有机会获取到锁
condition.signal();
// 等待其他线程唤醒
condition.await();
System.out.println(Thread.currentThread().getId()+"执行完毕!");
Thread.sleep(1000);
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 解锁,其他线程可获取到锁
reentrantLock.unlock();
}).start();
new Thread(()-> {
// 加锁
reentrantLock.lock();
System.out.println(Thread.currentThread().getId());
try {
Thread.sleep(1000);
// 唤醒其他线程,其他线程有机会获取到锁
condition.signal();
// 等待其他线程唤醒
condition.await();
System.out.println(Thread.currentThread().getId()+"执行完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
// 解锁,其他线程可获取到锁
reentrantLock.unlock();
}).start();
}
上面的小例子很简单,就是一个显示锁的应用小例子。其中lock即为加锁,作用等同于synchronized,signal为唤醒其他线程,相当于notify,await阻塞当前线程,即为wait。
输出:
10
11
10执行完毕!
11执行完毕!
默认ReentrantLock是非公平锁,即获取锁不是按照先到先得的流程,而是谁先抢到谁先占有。
NonfairSync即为非公平锁,其是ReentrantLock的内部类。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// cas操作将重入锁次数设置为1,如果cas成功,那么当前线程拿到了这把锁。
// 这块也就是非公平锁与公平锁不一致的地方,公平锁没有这个逻辑。因为非公平锁线程来了就会立马去获取锁,不会排队。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果没有拿到这把锁,说明已经有线程持有这把锁
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// 默认获取非公平锁
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
// 取得当前线程
final Thread current = Thread.currentThread();
// 取得当前锁重入次数
int c = getState();
// c=0说明当前没有线程拥有这把锁,此时快速通过cas获取这把锁
if (c == 0) {
// cas操作将重入次数更新为1
if (compareAndSetState(0, acquires)) {
// 更新独占锁标示为当前线程,此时即为成功获取锁。
setExclusiveOwnerThread(current);
return true;
}
}
// 如果上述没有获取到锁,那么判断当前独占锁是不是当前线程(也就是当前持锁的线程是不是当前线程)
else if (current == getExclusiveOwnerThread()) {
// 如果持有锁的线程正好是当前线程,那么将锁重入标示+1。即拿到了这把锁。
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
其中Sync实现了AQS。我们先来看lock流程。上面代码中关于lock的注释已经很清楚了:
- 首先cas操作将state值将从0设置为1,如果成功,那么将exclusiveOwnerThread变量赋值为当前线程。成功即为获取到锁。
- 否则执行acquire。acquire是AQS中已经实现了的方法,其中只需要子类实现其tryAcquire方法即可。所以我们就来看下tryAcquire方法,此方法其实就是调用了内部类Sync.nonfairTryAcquire方法。
- nonfairTryAcquire方法中,首先还是用cas操作将state值将0设置成1,并更新当前exclusiveOwnerThread的值。cas操作成功则获取锁成功,反之执行下面流程。
- 如果再次获取锁失败,那么会判断目前持有锁的线程是否为当前线程(因为ReentrantLock支持重入锁),如果为当前线程,那么将state+1,成功获取到锁,返回。否则获取锁失败。
释放锁流程即调用AQS的release方法。可以参照我之前的两篇文章。
了解了非公平锁,那么公平锁就很容易理解了。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取重入次数
int c = getState();
if (c == 0) {
// 重点看hasQueuedPredecessors方法:这个方法就是判断队列中是否还有等待获取锁的节点,
// 如果队列中还存在等待获取锁的节点,那么当前线程就不去竞争锁,获取锁的机会留给队列中等待的节点。其他处理和非公平锁是一致的。
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;
}
}
// AQS中的方法
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// 如果当前线程之前有一个排队的线程,则为{@code true},如果当前线程位于该队列的开头或该队列为空,则为{@code false}
return h != t && // 存在等待节点
// h.next == null说明恰巧有一个线程正在持有锁
// 或者不只有一个等待节点,并且s.thread != Thread.currentThread():说明第一个等待节点所属线程不属于当前线程(也就是不满足重入锁条件)
((s = h.next) == null || s.thread != Thread.currentThread());
}
上面注释相信已经很清楚了。公平锁与非公平锁获取锁的流程区别在于:公平锁获取锁之前会判断AQS等待队列中是否存在还在等待的节点,如果存在等待的节点,并且第一个等待获取锁的节点不是当前线程,那么返回true。
会不会觉得这块有些绕呢?为什么hasQueuedPredecessors方法能判断出存在比当前线程请求还早的线程呢,我们仔细想一下:
h != t:说明AQS等待队列中一定存在等待节点,那么此方法返回true,表示这个线程不能去获取锁;那么如果h=t,这个方法就直接返回false,表示这个线程可以获取锁(因为这个线程之前没有等待获取锁的线程了)。
那么h.next = null说明什么呢?说明恰巧此时有一个线程拿到了锁并且在执行线程,那么此时该方法返回true,这个线程不允许在去竞争锁;否则判断h.next.thread是否是当前请求的这个线程;如果是,那么该方法返回false,可以去获取锁。否则就不允许获取锁。
signal:唤醒其他等待获取锁的线程。相当于notify。
AQS:
public final void signal() {
// protected final boolean isHeldExclusively() {
// // While we must in general read state before owner,
// // we don't need to do so to check if current thread is owner
// return getExclusiveOwnerThread() == Thread.currentThread();
// }
// 判断是否是持有锁的线程,也就是说只有持有锁的线程才能唤醒其他线程;
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// firstWaiter:是AQS中条件等待队列中的头节点(上篇文章中提到过)。
Node first = firstWaiter;
// 头节点不等于空,将条件等待队列中的线程移至到AQS同步队列中,等待唤醒。
if (first != null)
doSignal(first);
}
// 递归处理条件等待队列中的线程,将线程移至到同步队列中,等待唤醒。
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
上面的注解已经很清楚了,signal做了以下操作:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程加入到条件等待队列
Node node = addConditionWaiter();
// 释放当前线程持有的锁,即使有重入锁,也全部释放;并且唤醒在同步线程中等待获取锁的线程。
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判断当前线程是否在同步队列中,当然是不在的,那么将当前线程阻塞。
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
上面重点部分已经有注释进行了说明,await主要处理逻辑如下:
还是看本文最开始给出的signal和await使用的小例子,接下来描述下signal、await是如果在两个线程之间切换锁来交替执行两个不同线程中的代码:
1、线程A拿到锁,执行线程逻辑;
2、此时线程B申请锁,但申请失败,会加入到同步队列并阻塞;
3、此时如果A线程调用signal方法,将在条件等待队列中的线程移至到同步队列,但是此时不会有线程在条件队列中;
4、接着A线程调用await方法阻塞当前线程,在await方法中会将A线程加入到条件等待队列,并释放当前线程的锁,唤醒同步队列中的线程,也就是B线程被唤醒,并阻塞当前线程A。
5、接着线程B被唤醒,并且获取到锁,执行线程B中的逻辑。
6、当B线程调用signal方法后,将条件队列中的线程移至到同步队列,也就是将线程B移至到同步队列。
7、当B线程调用await方法后,会将B线程加入到条件等待队列,并释放锁,唤醒同步队列中的线程A,并阻塞当前线程,此时线程A继续执行。
8、一旦线程A执行完毕,需调用signal方法后(将线程B从条件等待队列中移至到同步队列),然后调用unlock释放锁并唤醒线程B,线程B就可以继续执行了,直到执行结束。
本文主要回顾了Doug Lea公平锁与非公平锁设计思路与源码,下一篇将会继续带来JUC中门闩及信号量相关的设计思路与源码分析。如果本文对您还有一点点价值,不要忘了关注、点赞、分享。