前言
线程池是实现异步的重要手段之一,而根据《阿里巴巴java开发规范》,创建线程只能用线程池,而且这个线程池必须是通过ThreadPoolExecutor
来构造。而线程池的AQS并不是线程池的某个成员变量,我们简单的过一遍线程池的参数和执行流程。
一、线程池核心参数
- 线程池的核心线程数量:int corePoolSize
- 线程池的最大线程数:int maximumPoolSize
- 临时存活的最长时间:long keepAliveTime
- 时间单位:TimeUnit unit
- 任务队列,用来储存等待执行任务的队列:BlockingQueue workQueue
- 线程工厂,用来创建线程,一般默认即可:ThreadFactory threadFactory
- 拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务:RejectedExecutionHandler handler
二、线程池拒绝策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor
定义一些策略:
拒绝策略 | 说明 |
---|---|
AbortPolicy | 默认的拒绝策略,会抛出 RejectedExecutionException 异常。 |
CallerRunsPolicy | 在任务被拒绝添加后,会在调用者线程中执行该任务。 |
DiscardPolicy | 直接丢弃被拒绝的任务,不做任何处理。(不推荐) |
DiscardOldestPolicy | 丢弃等待队列中最久的任务,并将当前任务加入队列 |
三、线程池处理任务的流程
当我们要使用一个线程池来执行某一个线程的时候,我们通常会调用execute
或submit
方法,其中submit
的底层也是execute
,可以认为execute
是线程池中最重要的方法,而execute
方法的实现关键在于底层的addWorker方法,execute
的执行分为三步:
-
判断当前正在运行的线程数,如果当前线程数小于核心线程数,就调用
addWorker
方法创建一个线程来执行当前任务。这个过程中,addWorker
会做更深的判断,来决定是否可以创建线程 -
如果任务可以成功加入队列,再重新判断一次线程池的状态,再决定可不可以加入队列
-
如果无法无法入队就调用
addWorker
方法创建一个新的线程,如果失败就执行拒绝策略
(图源:JavaGuide)
四、阻塞队列
阻塞队列是线程池的一个参数,也是线程池的核心部分。 线程池中可以使用的阻塞队列有很多种,通过下图介绍一下阻塞队列的成员(图源:Java线程池实现原理及其在美团业务中的实践):
其中要补充的还有DelayedWorkQueue
,是ScheduledThreadPoolExecutor
中使用的阻塞队列。
这些队列中,通常长度为无界的或者是int的最大值的阻塞队列都不推荐使用,原因是容易造成OOM,特别是SynchronousQueue
,不仅是无界的,最大线程数也是int的最大值,带来的开销会更大。
下面来看一个简单的阻塞队列的实现:
csharp
public class BlockingQueue<T> {
//双向队列,两个条件变量:获取元素时队列为空要等待,添加元素时队列满了要等待
private Deque<T> queue = new ArrayDeque<>();
private int capacity = 5;
private ReentrantLock lock = new ReentrantLock();
private Condition emptyCondition = lock.newCondition();
private Condition fullCondition = lock.newCondition();
//获取元素
public T take(){
lock.lock();
try {
while (queue.isEmpty()){
//队列为空阻塞
try {
emptyCondition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//不为空
T t = queue.removeFirst();
fullCondition.signal();
return t;
} finally {
lock.unlock();
}
}
//添加元素
public void put(T element){
lock.lock();
try {
while(queue.size() == capacity){
try {
fullCondition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
queue.addLast(element);//添加到队尾
emptyCondition.signal();
} finally {
lock.unlock();
}
}
public int size(){
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
}
大部分阻塞队列的获取和添加元素的API都是这样实现的,可能有些人看到这段代码会思考一个问题:如果获取元素时队列为空阻塞了,那岂不是死锁了?
其实非也,这里就要说到一个经常和ReentrantLock
配合使用的类Condition
了,Condition
是一个接口,他的实现类ConditionObject
是AQS
中的一个类,本质上也是AQS的实现。如果调用了这个类的await
方法,当前线程会释放锁并进入一个等待队列,这个等待队列就是AQS
。ConditionObject
中显示地维护了队头指针和队尾指针,当调用signal
方法的时候就会唤醒队头节点的线程并获取锁,这就是不会死锁的原因。
总结
线程池中的AQS主要是存在于阻塞队列中,阻塞队列通过ReentrantLock和Condition配合实现,其中AQS则是实现于ConditionObject的条件队列中。