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

相关推荐
东华万里5 分钟前
第40篇C++核心基础与工程实践:从底层逻辑到避坑指南
开发语言·c++·面试·大学生专区
智驭未来掌门人29 分钟前
利用FFmpeg快速实现一个RTSP推流服务器
后端
垚垚学技术_聚焦云原生42 分钟前
K8s Pod 完整生命周期详解
后端
JavaGuide1 小时前
Github 史诗级故障,与此同时,Cursor 版「GitHub」正式上线!
前端·后端
不一样的少年_1 小时前
修了 Bug、做了重构,为什么老板还是觉得你没产出?
前端·后端·程序员
水深火乐1 小时前
安全地生成验证码、密码和 Token
后端
小强19881 小时前
SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程
后端
水深火乐1 小时前
interface和any
后端
步行cgn1 小时前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
神奇小汤圆1 小时前
Spring Boot + LangChain4j实现RAG——从零搭建企业级知识库问答系统
后端