从Java线程池到池化思想

 


Java线程创建和销毁的开销 中提到了Java线程创建和销毁的开销,因此我们可以使用“池”化的思想,每一次有新的任务需要处理时,直接从线程池中拿出来一个空闲的线程去执行,这样减少了创建线程的开销,同时在任务处理完成后将线程归还给线程池,给下一个任务使用,也减少了销毁的开销。

池化思想在我们开发中非常常见,比如:数据库连接池,Tomcat线程池,业务开发中的线程池,Netty的EventLoopGroup,甚至于HTTP的长连接也是复用TCP连接,和池化思想很相似。

为什么要使用线程池?

  1. 降低资源消耗,提高响应速度
  2. 如果任务多了,我们其实有一些衍生的管理、编排的需求,例如:【1】
    • 顺序管理。任务按照什么顺序执行?(FIFO、LIFO、优先级)?
    • 资源管理。同时有多少任务能并发执行?允许有多少任务等待?
    • 错误处理。如果负载过多,需要取消任务,应该选哪个?如何通知该任务?
    • 生命周期管理。如何在线程开始或结束时做一些操作?

线程池的参数

public ThreadPoolExecutor(int corePoolSize,                      // 核心线程数
                          int maximumPoolSize,                   // 最大线程数
                          long keepAliveTime,                    // 保持存活时间,当线程池的线程数多于corePoolSize时,当空闲线程的时间超过keepALiveTime时,多于corePoolSize数量的线程会被回收(如果设置某个参数的话,核心线程数也会被回收)。这个变量存在的意义,还是为了减少资源的消耗。
                          TimeUnit unit,                         // 时间单位
                          BlockingQueue<Runnable> workQueue,     // 任务存储队列
                          ThreadFactory threadFactory,           // 当线程池需要新的线程的时候,会使用threadFactory创建
                          RejectedExecutionHandler handler       // 任务拒绝策略
      ) {
  // ...
}

当新的任务来时的流程:

  1. 看当前的运行的线程数是不是小于corePoolSize,如果是的话就创建新的线程
  2. 如果目前运行的线程数等于corePoolSize的话,就看任务存储队列是不是空的,如果是的话,就把任务往这里面放
  3. 如果任务队列也满了,那就再创建新的线程,直至等于maximumPoolSize,这里队列可以是无界的,maxmumPoolSize也可以是很大的值
  4. 最后根据拒绝策略,去拒绝新来的任务

常见的BlockingQueue的实现类有:

  1. ArrayBlockingQueue,数组,ReentrantLock,有界
  2. LinkedBlockingQueue,链表,ReentrantLock,有界
  3. LinkedTransferQueue,链表,CAS,无界。需要注意的是,队列是无界的话,线程池等待队列有可能会变得越来越大,可能会导致频繁的GC,甚至于OOM

拒绝策略:

  1. 丢弃任务,不重要的任务,比如打印日志
  2. 调用方执行
  3. 抛异常

Executors中几种线程池

  1. // 可能占用的内存会很大
    newFixedThreadPool(n, n, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), default, default);
    
  2. newSingleThreadExecutor
    // 同样的问题
    new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), default, default);
    
  3. newCachedThreadPool
    // 线程数可能特别多,队列长度为0
    new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), default, default);
    
  4. newScheduledThreadPool
    // 同样的线程数可能特别多
    corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), default, default
    

所以最好根据实际的业务场景自定义一个线程池

停止线程池

  1. shutdown():现有的任务还是会执行,但不再接受新的任务
  2. isShutdown():来判断是不是执行过了shutdown()
  3. isTerminated():用于检查线程池是否已经终止。如果线程池的所有任务都已经执行完成并且线程池已经调用了 shutdown 方法,则返回 true;否则返回 false
  4. awaitTermination(long timeout, TimeUnit unit):用于等待线程池的所有任务执行完成,并设置等待指定的超时时间
  5. shutdownNow():用于立即关闭线程池。调用该方法会尝试停止所有正在执行的任务,并返回一个未执行的任务列表。该方法会使用 Thread.interrupt() 方法尝试中断正在执行的任务,但并不能保证任务一定会停止

线程池原理

实际问题

  1. 快速相应请求,如:请求A依赖于B、C、D,且B、C、D互相没有依赖关系,则可以将B、C、D放入到一个线程池中执行,这样相应的实现取决于最慢的任务。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务【2】。这个地方我认为可以这么理解:直接创建新的线程去执行是比放到任务队列中响应时间快的,但是前提是CPU的处理能力要很强,假设CPU只有一核,那么创建很多的线程提升的性能可能很有限甚至于没有提升,因为有部分时间在进行上下文切换。

  2. 快速处理批量任务,如统计报表,与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题【2】。吞吐量优先就需要

响应时间 vs 吞吐量,是两个矛盾的点,在垃圾回收算法和CPU的任务调度中也有体现,待补充...

数据库连接池

SpringBoot2默认连接池HikariCP【3】

Tomcat线程池

该部分参考【4】,算是笔记

Tomcat整体架构

Server                              // 一个JVM只有一个Server,
  Service                           // 服务的抽象
  Service
  .
  .
  Service
    Connetor                        // 处理连接请求
      HTTP、AJP
      阻塞IO,非阻塞IO
    Engine                          // 全局Servlet引擎,每个Service只能有一个
      Host                          // 代表虚拟主机,可以存放若干Web应用的抽象
      .
      .
      Host
        Context                     // Web应用的抽象,我们开发的Web应用部署到Tomcat后就会转化为Context
          Wrapper                   // 一个Servlet就对应着一个Wrapper
    Executor                        // 执行Service的任务

===================================

仅作为校招时的《个人笔记》,详细内容请看【参考】部分

===================================

参考

  1. https://lotabout.me/books/Java-Concurrency/Cost-of-Thread/index.html
  2. https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
  3. https://pdai.tech/md/spring/springboot/springboot-x-mysql-HikariCP.html
  4. 《Tomcat内核设计剖析》
posted @   optimjie  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示