线程池中的AQS

前言

线程池是实现异步的重要手段之一,而根据《阿里巴巴java开发规范》,创建线程只能用线程池,而且这个线程池必须是通过ThreadPoolExecutor来构造。而线程池的AQS并不是线程池的某个成员变量,我们简单的过一遍线程池的参数和执行流程。

一、线程池核心参数

  1. 线程池的核心线程数量:int corePoolSize
  2. 线程池的最大线程数:int maximumPoolSize
  3. 临时存活的最长时间:long keepAliveTime
  4. 时间单位:TimeUnit unit
  5. 任务队列,用来储存等待执行任务的队列:BlockingQueue workQueue
  6. 线程工厂,用来创建线程,一般默认即可:ThreadFactory threadFactory
  7. 拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务:RejectedExecutionHandler handler

二、线程池拒绝策略

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor 定义一些策略:

拒绝策略 说明
AbortPolicy 默认的拒绝策略,会抛出 RejectedExecutionException 异常。
CallerRunsPolicy 在任务被拒绝添加后,会在调用者线程中执行该任务。
DiscardPolicy 直接丢弃被拒绝的任务,不做任何处理。(不推荐)
DiscardOldestPolicy 丢弃等待队列中最久的任务,并将当前任务加入队列

三、线程池处理任务的流程

当我们要使用一个线程池来执行某一个线程的时候,我们通常会调用executesubmit方法,其中submit的底层也是execute,可以认为execute是线程池中最重要的方法,而execute方法的实现关键在于底层的addWorker方法,execute的执行分为三步:

  1. 判断当前正在运行的线程数,如果当前线程数小于核心线程数,就调用addWorker方法创建一个线程来执行当前任务。这个过程中,addWorker会做更深的判断,来决定是否可以创建线程

  2. 如果任务可以成功加入队列,再重新判断一次线程池的状态,再决定可不可以加入队列

  3. 如果无法无法入队就调用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是一个接口,他的实现类ConditionObjectAQS中的一个类,本质上也是AQS的实现。如果调用了这个类的await方法,当前线程会释放锁并进入一个等待队列,这个等待队列就是AQSConditionObject中显示地维护了队头指针和队尾指针,当调用signal方法的时候就会唤醒队头节点的线程并获取锁,这就是不会死锁的原因。

总结

线程池中的AQS主要是存在于阻塞队列中,阻塞队列通过ReentrantLock和Condition配合实现,其中AQS则是实现于ConditionObject的条件队列中。

相关推荐
血小板要健康3 小时前
Java基础常见面试题复习合集1
java·开发语言·经验分享·笔记·面试·学习方法
野犬寒鸦5 小时前
从零起步学习并发编程 || 第一章:初步认识进程与线程
java·服务器·后端·学习
我爱娃哈哈5 小时前
SpringBoot + Flowable + 自定义节点:可视化工作流引擎,支持请假、报销、审批全场景
java·spring boot·后端
李梨同学丶7 小时前
0201好虫子周刊
后端
思想在飞肢体在追7 小时前
Springboot项目配置Nacos
java·spring boot·后端·nacos
cyforkk7 小时前
09、Java 基础硬核复习:异常处理(容错机制)的核心逻辑与面试考点
java·数据库·面试
Loo国昌9 小时前
【垂类模型数据工程】第四阶段:高性能 Embedding 实战:从双编码器架构到 InfoNCE 损失函数详解
人工智能·后端·深度学习·自然语言处理·架构·transformer·embedding
ONE_PUNCH_Ge10 小时前
Go 语言泛型
开发语言·后端·golang
Warren9810 小时前
Pytest Fixture 作用域详解:Function、Class、Module、Session 怎么选
面试·职场和发展·单元测试·pytest·pip·模块测试·jira
良许Linux10 小时前
DSP的选型和应用
后端·stm32·单片机·程序员·嵌入式