线程池技术记录

使用线程池的好处

  • 降低资源消耗;降低频繁的创建、销毁线程带来的额外开销,复用已创建的现场。
  • 降低使用的复杂度;将任务的提交和执行进行解耦,我们只需要创建一个线程池,然后往里面添加任务即可,具体的执行流程由线程池自己管理,降低使用复杂度。
  • 提高线程的可管理性;能有效安全的管理线程资源,避免不加限制无限的申请资源,造成资源耗尽的风险。
  • 调高响应速度;任务到达后,直接复用已经创建好的线程执行。

线程池通常有哪些使用场景

  • 快速响应用户请求,响应速度优先;
    比如一个用户请求,需要通过RPC去调用好几个服务获取数据然后聚合返回,此场景就可以通过线程池并行调用,响应时间取决于响应最慢的那个RPC接口的耗时;
    又或者一个注册请求,注册完成后需要发送短信、邮件通知,为了快速返回给用户,可以将通知的操作丢到线程池里异步的去执行,然后直接返回客户端成功,提高用户体验。
  • 单位时间处理更多请求,吞吐量优先;
    比如接收MQ的消息,然后去调用第三方的接口查询数据,此场景并不追求快速响应,主要利用有限的资源在单位时间内处理尽可能多的任务,可以利用任务队列进行任务的缓冲

ThreadPoolExecutor的核心参数

corePoolSize //核心线程数
maximumPoolSize //最大线程数
keepAliveTime //空闲线程超时时间
unit //时间单位
workQueue //阻塞队列
handler //拒绝策略
ThreadFactory //线程工厂
  • execute()方法执行逻辑
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

1、判断线程池状态,如果不是RUNNING状态,直接执行拒绝策略;
2、如果当前线程数<核心线程数,则新建一个线程来处理提交的任务;
3、如果当前线程数>核心线程数且任务队列没满,则将任务放入阻塞队列等待执行;
4、如果核心线程数<当前线程数<最大线程数,且任务队列已满,则创建新的线程执行提交的任务;
5、如果当前线程数>最大线程数,且队列已满,则执行拒绝策略拒绝该任务;

  • 上述执行流程是JUC标准线程池提供的执行流程,主要在CPU密集型场景下使用。
    想Tomcat、Dubbo这类框架,他们的内部线程池主要是用来处理网络IO任务的,所以他们都对JUC线程池的执行流程进行了调整来支持IO密集型场景使用。
    他们提供了阻塞队列TaskQueue,该队列继承了LinkedBlockingQueue,重写了offer()方法来实现执行流程的调整。
@Override
    public boolean offer(Runnable o) {
        //we can't do any checks
	//parent就是所属的线程池对象
	//如果parent为null,直接调用父类的offer方法入队
        if (parent==null) return super.offer(o);
        //we are maxed out on threads, simply queue the object
	//如果当前线程数等于最大线程数,则直接调用父类的offer方法
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        //we have idle threads, just add it to the queue
	//如果未执行的任务数量小于当前线程数,则直接调用父类offer的方法入队,因为有空闲线程,一入队任务立马被执行
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
        //if we have less threads than maximum force creation of a new thread
	//如果当前线程数小于最大线程数,返回false,然后添加新的线程执行任务
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        //if we reached here, we need to add it to the queue
	//其他情况直接入队
        return super.offer(o);
    }
  • 当前线程数大于核心线程数是,JUC包的原生线程池首先是把任务放到任务队列里等待执行;如果Tomcat也这样做的话,会降低请求的整体响应速度,所以启对线程池的阻塞队列的offer()方法重写,修改后Tomcat的线程池执行流程如下:
1、当前线程数小于核心线程数,则新建一个线程来处理提交的任务
2、当前线程数大于核心线程数,小于最大线程数,则创建一个新的线程来处理提交的任务
3、如果当前线程数等于最大线程数,则任务入队等待执行
4、如果队列已满,执行拒绝策略
  • 线程池的Worker线程模型,继承了AQS实现了锁机制。
    线程启动后执行runWorker()方法,runWorker()方法中调用了getTask()方法从阻塞队列中获取任务,拿到任务后先执行beforeExecute()钩子函数,再执行任务,然后再执行afterExecute()钩子函数。若超时获取不到任务会调用processWorkerExit()方法执行Worker线程的清理工作。

阻塞队列 - 常用的阻塞队列

阻塞队列BlockingQueue继承Queue。
从阻塞队列获取数据是,如果队列为空,则等待直到队列有元素存入。当向阻塞队列中存入元素是,如果队列已满,则等待直到队列中有元素呗移除。提供了offer()、put()、take()、poll()等常用方法。

  • JDK提供的阻塞队列的实现有以下几种:
    1、ArrayBlockingQueue:由数组实现的有界阻塞队列,该队列按照FIFO对元素进行排序。维护了两个整形变量,标识队列头尾在数组中的位置,在生产者放入和消费者获取数据共用一个锁对象,意味着两者无法真正的并行运行,性能较低。
    2、LinkedBlockingQueue:由链表组成的有界阻塞队列,如果不指定大小,默认使用Integer.MAX_VALUE作为队列大小,改队列按照FIFO对元素进行排序,对生产者和消费者分别维护了独立的锁来控制数据同步,意味着该队列有着更高的并发性能。
    3、SynchronousQueue:不存储元素的阻塞队列,无容量,可以设置公平或非公平模式,插入操作必须等待获取操作移除元素,反之亦然。
    4、PriorityBlockingQueue:支持优先级排序的无界阻塞队列,默认情况下根据自然序排序,也可以指定Comparator。
    5、DelayQueue:支持延时获取元素的无界阻塞队列,创建元素时可以指定多久之后才能从队列中获取元素,常用于缓存系统或定时任务调度系统。
    6、LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,与LinkedBlockingQueue相比多了transfer和tryTransfer方法,该方法在有消费者等待接收元素是会立即将元素传递给消费者。
    7、LinkedBlockingDeque:一个由链表结构组成的双端阻塞队列,可以从队列的两端插入和删除元素。

Woker继承了AQS实现了锁机制,那ThreadPoolExecutor都用到哪些锁?为什么要用锁?

posted @ 2022-09-15 15:13  阿步呦  阅读(51)  评论(0编辑  收藏  举报