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

相关推荐
Moment15 小时前
Cursor 的 5 种指令方法比较,你最喜欢哪一种?
前端·后端·github
IT_陈寒15 小时前
Vite快得离谱?揭秘它比Webpack快10倍的5个核心原理
前端·人工智能·后端
摸鱼的春哥16 小时前
Agent教程17:LangChain的持久化和人工干预
前端·javascript·后端
哈里谢顿16 小时前
0304面试kimi总结归纳版
面试
哈里谢顿16 小时前
0304面试千问总结归纳版
面试
风象南16 小时前
OpenClaw 登顶 GitHub Star 榜首:一个程序员 13 年后的"重新点火"故事
人工智能·后端
Victor35616 小时前
MongoDB(25)什么是单字段索引?
后端
Victor35616 小时前
MongoDB(26)什么是复合索引?
后端
程序员爱钓鱼17 小时前
Go操作Excel实战详解:github.com/xuri/excelize/v2
前端·后端·go
oak隔壁找我1 天前
MySQL中 SHOW FULL PROCESSLIST` 输出中 `State` 列的所有可能值
后端