线程池中的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的条件队列中。

相关推荐
yanlele4 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
ai小鬼头4 小时前
Ollama+OpenWeb最新版0.42+0.3.35一键安装教程,轻松搞定AI模型部署
后端·架构·github
萧曵 丶4 小时前
Rust 所有权系统:深入浅出指南
开发语言·后端·rust
老任与码5 小时前
Spring AI Alibaba(1)——基本使用
java·人工智能·后端·springaialibaba
小兵张健5 小时前
武汉拿下 23k offer 经历
java·面试·ai编程
华子w9089258595 小时前
基于 SpringBoot+VueJS 的农产品研究报告管理系统设计与实现
vue.js·spring boot·后端
爱莉希雅&&&5 小时前
技术面试题,HR面试题
开发语言·学习·面试
天天扭码6 小时前
《很全面的前端面试题》——HTML篇
前端·面试·html
星辰离彬6 小时前
Java 与 MySQL 性能优化:Java应用中MySQL慢SQL诊断与优化实战
java·后端·sql·mysql·性能优化
GetcharZp7 小时前
彻底告别数据焦虑!这款开源神器 RustDesk,让你自建一个比向日葵、ToDesk 更安全的远程桌面
后端·rust