背景:
JUC并发包提供了很多线程并发问题的解决方案,包括ConcurrentHashMap哈希Map,ConcurrentLinkedDeque阻塞队列,Executor线程池以及locks包,Atomic原子类的包等数据结构和逻辑思想,本章节主要讨论AbstractQueuedSynchronizer(AQS),基于AQS实现的Lock锁,以及基于AQS和Lock锁扩展的Condition等待队列,其余部分会有专门章节分析。
一. AQS(AbstractQueuedSynchronizer)
1. AQS究竟是什么:
AbstractQueuedSynchronizer,抽象队列同步器,看名字比较抽象,其实他只是一种数据结构和设计思想,没有什么实质性的功能,但是基于AQS我们可以实现很多想要实现的功能和逻辑,比如我们下文会介绍的Lock锁和线程池中的worker工作者线程等等。
2. AQS的逻辑与思想(结合源码):
首先,AQS维护一个和CLH队列,我更习惯称它为双向同步队列,队列都是由一个个Node节点组成:
static final class Node {...}
先不关心这个节点里面由什么信息,先把整体构思理解,再讨论里面的内容
然后,这些节点以双向链表的结构连接起来:
从图中我们能看出来,既然它是一个双向链表,那么每一个Node节点肯定要保存前置节点和后置节点,同时,它既然是解决线程并发问题,那么node节点还要保存任务线程和该任务线程的状态,接下来我们剖析Node的信息:
static final class Node {
...
// 该节点的线程是取消状态
static final int CANCELLED = 1;
// 表明下一节点的线程是可唤醒状态
static final int SIGNAL = -1;
// 表明当先节点是Condition队列中
static final int CONDITION = -2;
// 等待状态
volatile int waitStatus;
// 前置节点
volatile Node prev;
// 后置节点
volatile Node next;
// 当前任务线程对象
volatile Thread thread;
...
}
从node定义中我们能看到,每一个node节点不仅保存了prev节点和next节点,还保存了当前node的任务线程对象,还有当前节点的state状态,同时,还有一点要注意的就是,AQS队列,会初始化一个Head头节点和Tail尾节点,而Head头节点是永远保持空节点的,利用图表示出来就是:
这张图大致就是AQS的核心了,AQS会维护这个双向同步队列,Head节点会永远指向第一个节点,而且这个节点是空节点,当有新的节点进来的时候,Tail尾节点会指向最后一个节点,并且这个节点保存了任务线程和等待状态,而且,AQS还会维护一个正在执行任务的线程,因为不可能所有线程都处于同步队列中等待,所以还会有 exclusiveOwnerThread 对象指向正在执行的任务,还有 state 值在lock锁中表示当前线程是否持有锁,以及重入的次数,当state=0,表示没有持有锁,当state>0表示持有锁,并且state的值就是持有锁之后的重入次数。当然了,AQS还对提供了很多方法和API,在下面的Lock剖析中,会进一步说明,包括我们利用这个队列做了什么操作,这个队列是如何帮我们实现线程阻塞等待和锁功能的。
二. ReentrantLock
Lock接口提供了很多实现,包括独占锁和共享锁,也就是我们说的读锁和写锁,我们这里只讨论ReentrantLock锁的实现。
1.知识点回顾:
在介绍ReentrantLock锁之前,我们要回顾一个知识点,就是Thread.interrupt() 线程请求中断 和 Thread.interrupted() 线程响应中断,首先,interrupt()方法会去中断一个线程A,这里的中断不是强制中断,而是发出一个中断请求,将线程A的中断状态置为 true ,此时,如果线程A调用了interrupted()方法,就会拿到这个中断状态,然后去响应这个中断请求,并且将中断状态还原为 false,如果线程A此时正在sleep(),则会推出睡眠,抛出 InterruptedException 异常,然后在异常捕获中去响应中断,而中断指令是native本地方法,所以不做过多说明。
2. ReentrantLock剖析:
首先,从源码中我们能看出来,维护了一个Sync 抽象内部类,这个就是基于AQS实现的:
abstract static class Sync extends AbstractQueuedSynchronizer {...}
// 公平锁
static final class FairSync extends Sync {...}
...
// 非公平锁
static final class NonfairSync extends Sync {...}
而这个抽象内部类有两个子类,就是我们常说的公平锁与非公平锁,这是典型的模方法模式的实现,根据不同的需要,创建公平与非公平锁,根据创建Lock锁对象的时候,调用构造函数,true就是公平锁,false就是非公平锁。
// 创建一个公平锁
ReentrantLock lock = new ReentrantLock(true);
// 创建一个非公平锁
ReentrantLock lock = new ReentrantLock(false);
那么他们如何区分公平与非公平的,先卖个关子,下面我们会讲到。
从主线出发, 我们直接从加锁的方法进入,
lock.lock():
public void lock() {
sync.lock();
}
从这里我们可以看到其实ReentrantLock 的lock()加锁,其实就是调用的sync的lock方法,而sync有FairSync 子类和 NonfairSync子类,我们先从FairSync公平锁的逻辑进去:
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
...
}
这里可以看到,FairSync的lock方法 其实调用的是AQS的acquire方法,并且传了一个1的值进去,这个方法的核心作用是创建一个Node节点加入到AQS同步队列,我们继续点进去看:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
进入tryAcquire(arg)方法:
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()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 增加重入次数
setState(nextc);
return true;
}
// 未获取到锁 返回false
return false;
}
从上面的代码,我们能了解到,当线程去获取锁的时候,会判断当前线程是否已经获取到了锁,如果没有获取到锁回去抢夺锁,如果已经持有了锁,会增加重入次数。
进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法:
其中addWaiter(Node.EXCLUSIVE),Node.EXCLUSIVE = null 就是表示创建一个 排他锁 的Node节点,这里不做过多叙述,说白了就是创建一个Node节点,调用AQS的 enq(final Node node) 方法放入AQS队列的尾部,可以回想下AQS的队列结构。主要看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;
// 返回中断状态
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
从上面代码中 我们能看到创建了一个Node节点加入到AQS队列之后,当前线程会以 自旋 的方式去判断创建的Node节点是不是Head节点后面的第一个节点,我们之前说过,Head节点是空节点,所以,如果当前线程是Head后的第一个节点,说明是队列中的第一个任务线程Node,因此可以去尝试获取锁,如果成功了,会返回出去,并且携带响应中断的状态,往下走,先到达 shouldParkAfterFailedAcquire(p, node) 方法,这个方法主要是清楚已经失效的Node节点,就是状态为 CANNEL的节点,我们主要看一下parkAndCheckInterrupt() 方法:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这个我们终于看到了重点中的重点,线程去调用 LockSupport.park(this) 方法进行阻塞,因为当一个线程加入到了AQS队列之后,如果没有获取到锁,那么必须被阻塞起来,等待被唤醒。结合上面的代码逻辑,我们大概明白了,比如两个线程,第一个线程获取到了锁,则执行任务,而第二个线程没有获取到锁,则加入到AQS队列进行等待,并且被park方法阻塞,等待被唤醒:
大家请记住这个地方,因为线程B在这里被阻塞,当他被唤醒的时候,会接着从这里往下走。
lock.unlock():
public void unlock() {
sync.release(1);
}
同样的,我们能看到unlock方法仍然走的是sync的方法,同lock方法对比,我们能推算出来,sync.release(1)方法,其实走的就是AQS的方法。
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(arg) 的代码就不贴了,他的作用是首先去释放锁,如果重入次数大于1,则对重入次数减1,不然就释放锁,并且将exclusiveOwnerThread置空,同时设置state值等于0,表示没有线程持有锁,其他线程可以来抢夺锁。接下来,如果释放锁成功,就执行 unparkSuccessor(h) 方法,这里我们能猜到,既然释放了锁,那么一定是去唤醒之前因为没有抢夺到锁而被阻塞的线程,让我们点进去看一下:
private void unparkSuccessor(Node node) {
...
LockSupport.unpark(s.thread);
...
}
其他的部分我们先不管,这里能看到,果然和我们猜想的一样, LockSupport.unpark(s.thread) 是 线程A 释放锁之后,去唤醒阻塞在AQS队列中的Node节点的 线程B ,这里和上面我们说的 LockSupport.park(this) 相对应。
那么我们继续回到之前因为没有抢夺到锁而被阻塞的地方:
private final boolean parkAndCheckInterrupt() {
// 之前的线程B被阻塞在了这里
LockSupport.park(this);
return Thread.interrupted();
}
因为线程B被唤醒,所以它会接着执行 “return Thread.interrupted()”这一行代码,为什么要获取中断状态,这是因为,正常情况下,线程B是线程A释放锁之后,主动唤醒的,还有一种可能性,就是线程B是被其他线程发出了中断请求而唤醒的,所以这里要判断,线程B究竟是被正常唤醒,还是被中断唤醒的,线程B在这里返回出去,接下来又回到了acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这里
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;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
// 被唤醒后回到了这里
parkAndCheckInterrupt())
// 如果是被中断唤醒的,那么执行下面的代码
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
主要看注释的那个地方,线程B被唤醒后返回出来,如果是被中断唤醒的,那么parkAndCheckInterrupt()的返回值肯定是true,就是interrupted会被设置成true,保存线程B是被中断唤醒的状态,而由于自旋的存在,线程B会进入到下一次循环,这里需要注意的是,线程B虽然被唤醒,但是它还是需要去抢夺锁,抢夺到锁之后才会执行业务所及,并不是说线程B被线程A唤醒之后就执行执行业务逻辑,假设线程B抢夺到了锁之后,会返回出去,并且携带中断状态信息,继续往回走:
acquire()方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// 返回到了这里
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
从代码能看到,线程B被唤醒后,如果抢夺到了锁,则会回到注解的地方,然后执行 selfInterrupt() 方法,还记得文章前面说的,为什么要返回响应中断信息吗,就是在这里做的逻辑判断,如果是被中断唤醒,则会执行 selfInterrupt() 方法
selfInterrupt()方法:
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
这里可能有读者有疑问,为什么还要中断一次,不是已经拿到中断信息了吗,这个地方设计的很巧妙,因为我们之前在Thread.interrupted() 方法已经拿到了中断状态,但是在拿到中断状态的同时,也把中断状态重置成了false,那么如果我们在业务代码中,有中断状态的判断,这里就会出现问题,所以,当lock锁知道线程B被中断了之后,会重新设置线程B的中断状态,保证线程B中的业务逻辑是能知道线程B是被正常唤醒还是被中断唤醒。
3. 公平与非公平的体现
在使用ReentrantLock锁的时候,可能有些人会认为非公平锁就是AQS中所有的等待线程都是平等的,都是可以去抢夺锁的,其实并不是的,这里要注意的是,在非公平锁的情况下,只要位于AQS队列中的线程,只有Head节点后面的第一个Node可以去抢夺锁,其实对于AQS队列来说,他们只有公平,没有非公平,那么这个非公平体现在哪里,主要体现在:比如现在AQS中第一个等待节点是Thread
B,当ThreadA释放锁之后,唤醒B去抢夺锁,而这个时候,又来了一个线程C,线程C一上来并不是直接加入到AQS队列,而是和线程B一块去抢夺锁,所以说非公平性体现在这里,看代码:
static final class NonfairSync extends Sync {
...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
static final class FairSync extends Sync {
...
final void lock() {
acquire(1);
}
}
看代码能看出来,非公平锁上来就会去抢夺锁,而非公平锁会先考虑加入到AQS队列中。再强调一遍,已经加入到AQS队列中的线程,还是会以先进先出的方法去抢夺锁,并没有非公平性。
三. Condition
1. Condition介绍
Condition是由Lock去创建和维护的一个 单向阻塞队列 ,通常利用该队列实现多线程环境下的 生产者消费者 模型。
方法 | 作用 |
---|---|
await() | 阻塞当前线程 |
await(long time, TimeUnit unit) | 带过期时间阻塞当前线程 |
signal() | 唤醒阻塞线程 |
signalAll() | 唤醒所有阻塞线程 |
… | … |
接下来开始分析源码和逻辑实现:
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
...
}
首先,Condition是一个单向的阻塞队列,所以它只有nextWaiter节点,并不像AQS既有prev节点和next节点,其次,这里的Node节点其实是复用的AQS中的Node节点,所以刚开始看AQS源码的时候,会发现Node节点有很多属性,其实并不是所有的属性都会在一个地方用到,他们很多都是应用在不同的功能点中的。
Condition condition = lock.newCondition();
可以看出来,一个lock锁可以创建多个Condition队列,所以我们能推断出来,一个lock可以实现多个并行生产者消费者模型。
Demo:
// 消费者
lock.lock();
try{
for(;;){
if(LockCondition.pool.get() <= 0){
condition.await();
}
LockCondition.pool.set(LockCondition.pool.get() - 1);
System.out.println("消费消息 : " + LockCondition.pool.get());
condition.signal();
}
}catch (Exception e){
e.printStackTrace();
}finally {
condition.signal();
lock.unlock();
}
// 生产者
lock.lock();
try{
for(;;){
if(LockCondition.pool.get() >= 1){
condition.await();
}
LockCondition.pool.set(LockCondition.pool.get() + 1);
System.out.println("生产者生产消息 : " + LockCondition.pool.get());
condition.signal();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
这个demo实现了 PV 生产者消费者模型,资源 S 这里用LockCondition.pool(AtomicInteger(0))设置的。
同样的,我们从 condition.await() 方法进入,看看它做了什么:
await():
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);
}
分析:
首先会调用 addConditionWaiter() 方法创建一个Condition阻塞队列的Node节点,并且加入到Condition队列中;
然后调用 fullyRelease(node) 方法,根据名字,我们试着去推断一下,既然当前线程要主动阻塞,那么它肯定会去释放所持有的lock锁,然后去唤醒被阻塞在AQS中的等待线程,让我们点进去看一下,验证一下猜想:
final int fullyRelease(Node node) {
// 失败状态
boolean failed = true;
try {
// 重入次数
int savedState = getState();
// 释放锁
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
从代码中,我们可以看出来,该方法逻辑就是去释放锁,为什么要获得重入次数呢,一个原因是既然要释放锁,必须要知道该锁不管被重入多少次,都应该 直接释放 ,另外一个原因是,要保存重入次数,当该线程下一次获得锁的时候,要 恢复之前的重入次数 。
接着我们进入到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;
}
private void unparkSuccessor(Node node) {
...
LockSupport.unpark(s.thread);
}
当进入到unparkSuccessor()方法之后,我们就验证了猜想,这里确实是去唤醒在AQS中处于阻塞的线程节点去抢夺锁并且执行业务逻辑。
signal():
与await()对应的是signal()或者signalAll()方法,是要去唤醒在Condition队列中处于阻塞的线程节点,那么我们继续解析signal()方法的源码逻辑:
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 接着进入在这个方法
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 接着进入transferForSignal(first)方法
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
...
Node p = enq(node);
...
}
这一块的逻辑比较简单,我们直接进入到 transferForSignal(Node node) 方法,思绪就清晰了,在这里最主要做了:调用enq(node)方法将Condition队列中的头节点放入到AQS中队列中,这样它才有抢夺锁的资格
总结:
- AQS主要维护一个双向同步队列
- Lock锁基于AQS实现可重入的公平锁与非公平锁
- 当线程没有抢夺到锁,则会被包成Node节点加入到AQS队列,同时被挂起阻塞
- 当持有锁的线程调用unlock()方法释放锁后,会唤醒处于AQS中阻塞的线程,自旋的方式再去抢夺锁并且执行业务逻辑
- 当lock锁扩展Condition队列之后,可以实现生产者消费者模型,而这个模型是基于lock锁的
- 当持有锁的线程调用await()方法主动阻塞自己之后,会将自己包装成Condition队列中的Node节点,放到Condition队列中,并且释放锁
- 是线程调用singal()方法后,会将处于Condition队列中的第一个节点,转移到AQS中的Tail节点后,让该节点拥有抢夺锁的资格。
最后附上一张逻辑图:
由于这一块的源码很多很细,大家感兴趣的话可以自己过一下,这里会有一些细节没有说到,欢迎大家补充和纠正。