你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

JAVA线程池原理与使用

2021-11-13 22:50:10

文章目录

  • 一、什么是线程池?
    • 1.线程池介绍
    • 2.线程池使用场景
    • 3.线程池的好处
  • 二、线程池运行流程
    • 1.构造函数的7个核心参数
    • 2.线程池工作流程
    • 3.线程池创建方式
      • 正确的创建线程池的方法
    • 4.线程池状态转换
    • 5.线程池阻塞队列
        • 线程池为什么使用阻塞队列?
    • 6.线程池拒绝策略
        • 拒绝时机
        • 4种拒绝策略
    • 7.线程池钩子方法
  • 三、线程池监控
      • 线程池监控参数


一、什么是线程池?

1.线程池介绍

线程池,thread pool,是一种线程使用模式,线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

2.线程池使用场景

线程池解决的核心问题就是资源管理问题。
场景1:快速响应用户请求 将调用封装成任务并行的执行; 不设置队列,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务;
场景2:快速处理批量任务 任务量大,不需要瞬时完成,而是关注如何使用有限的资源; 设置队列去缓冲并发任务,合适的corePoolSize;
场景3:异步调用

3.线程池的好处

1. 复用线程,避免反复创建和销毁线程带来的开销问题,减少JVM回收垃圾的压力
2. 加快响应速度
3. 合理利用CPU和内存
4. 统一管理线程

二、线程池运行流程

1.构造函数的7个核心参数

参数名类型含义
corePoolSizeint核心线程数
maxPoolSizeint最大线程数
keepAliveTimelong保持存活时间
unitTimeUnit存活时间的单位
workQueueBlockingQueue任务存储队列
threadFactoryThreadFactory当线程池需要新的线程的时候,会使用threadFactory来生成新的线程
HandlerRejectedExecutionHandler由于线程池无法接受你所提交的任务的拒绝策略

corePoolSize:指的是核心线程数,线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,再创建新线程去执行任务

maxPoolSize:线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限

keepAliveTime:如果线程池当前的线程数多于 corePoolSize,那么如果多余的线程空闲时间超过 keepAliveTime,它们就会被终止

workQueue:工作队列 有3种最常见的队列类型∶

  • 直接交接:SynchronousQueue,需要将 maximumPoolSize 设置的大一些,因为这种队列本身不会进行任务缓存。
  • 无界队列:LinkedBlockingQueue,无需设置 maximumPoolSize,因为这种队列是一种无界队列,永远不会满,如果处理任务速度跟不上添加队列的速度,会导致队列中任务挤压严重。
  • 有界的队列:ArrayBlockingQueue,按需设置即可,maximumPoolSize 大于 corePoolSize。

ThreadFactory:用来创建线程新的线程是由 ThreadFactory 创建的,默认使用 Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的 NORM_PRIORITY 优先级并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等。通常我们用默认的ThreadFactory就可以了

2.线程池工作流程

当调用execute方法添加任务时,线程池会判断当前运行线程是否小于corepoolsize,如果小于创建线程执行;
如果不小于则将当前任务添加到队列中;
如果队列满了线程数小于maximumpoolsize,创建非核心线程执行任务;
如果队列满了非核心线程满了启动拒绝策略;
一个线程完成任务时,它会从队列中取下一个任务来执行。
线程空闲超过keepalivetime并且非核心线程会被回收。

  • 是否需要增加线程的判断顺序是:
    1.corePoolSize
    2.workQueue
    3.maxPoolSize

3.线程池创建方式

线程池应该手动创建还是自动创建?
手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险。

先来看看自动创建线程池, JDK为我们封装好了四种线程池使用方式:CachedThreadPool, FixedThreadPool, ScheduledThreadPool, SingleThreadExecutor。

  • FixedThreadPool
    固定线程数量的线程池;由于传进去的 LinkedBlockingQueue 是没有容量上限的,所以当请求数越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致OOM。

  • SingleThreadExecutor
    单线程的线程池:它只会用唯一的工作线程来执行任务。
    它的原理和 FixedThreadPool 是一样的,但是此时的线程数量被设置为了1。同样会产生OOM。

  • CachedThreadPool
    可缓存线程池
    特点:无界线程池,具有自动回收多余线程的功能
    缺点:这里的弊端在于第二个参数 maximumPoolSize 被设置成 Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致OOM。

  • ScheduledThreadPool
    支持定时及周期性任务执行的线程池

正确的创建线程池的方法

  • 根据不同的业务场景,自己设置线程池参数,比如我们的内存有多大。
  • 线程池里的线程数量设定为多少比较合适?
    CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2 倍左右。
    耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于 cpu 核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,推荐的计算方法:线程数=CPU核心数(1+平均等待时间/平均工作时间)

4.线程池状态转换

线程可以有如下5种状态:New 、Runnable 、Running 、Blocked 、Dead。
线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //线程池中的任务数
  • Running
    状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
    状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
  • ShutDown
    状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
    状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
  • Stop

    状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
    状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
  • Tidying

    状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
    状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
  • Terminated

    状态说明:线程池彻底终止,就变成TERMINATED状态。
    状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

5.线程池阻塞队列

  • FixedThreadPool 和SingleThreadExecutor 的 Queue 是LinkedBlockingQueue,基于链表 无界。
  • CachedThreadPool 使用的 Queue 是 SynchronousQueue,只有一个元素 同步。
  • ScheduledThreadPool来说,它使用的是延迟队列 DelayedWorkQueue。
  • workStealingPool 是JDK1.8加入的
    这个线程池和之前的都有很大不同。 子任务:当前线程中嵌套其他任务。 窃取:如果有线程处于空闲状态,会从其他线程的子任务中窃取线程来执行,但是不保证执行的顺序
  • ArrayBlockingQueue: 基于数组 有界。

线程池为什么使用阻塞队列?

队列满阻塞避免资源浪费

  • 阻塞队列原理
    ArrayBlockingQueue原理: get,put操作加锁; 执行完成notiyAll。
    put: 先获取可中断锁,判断数组长度与当前元素个数,如果相等notFull.await等待,insert(e)插入元素(插入完成notEmpty.signal唤醒正在等待的线程),解锁。
    take: 同put类似

6.线程池拒绝策略

拒绝时机

1)当 Executor 关闭时,提交新任务会被拒绝。 2)当 Executor 对最大线程和工作队列容量使用有限边界并且已经饱和时。

4种拒绝策略

AbortPolicy:直接抛出异常。 DiscardPolicy:丢弃一些队列中的任务。 DiscardOldestPolicy:丢弃最老未执行的任务。 CallerRunsPolicy:反馈到提交任务线程,由提交任务线程去执行。

7.线程池钩子方法

java.util.concurrent.ThreadPoolExecutor#beforeExecute
java.util.concurrent.ThreadPoolExecutor#afterExecute
java.util.concurrent.ThreadPoolExecutor#terminated

三、线程池监控

线程池使用不当也会使服务器资源枯竭,导致异常情况的发生,比如固定线程池的阻塞队列任务数量过多、缓存线程池创建的线程过多导致内存溢出、系统假死等问题。因此,我们需要一种简单的监控方案来监控线程池的使用情况,比如完成任务数量、未完成任务数量、线程大小等信息。

线程池监控参数

线程池提供了以下几个方法可以监控线程池的使用情况:

方法含义
getActiveCount()线程池中正在执行任务的线程数量
getCompletedTaskCount()线程池已完成的任务数量,该值小于等于taskCount
getCorePoolSize()线程池的核心线程数量
getLargestPoolSize()线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize
getMaximumPoolSize()线程池的最大线程数量
getPoolSize()线程池当前的线程数量
getTaskCount()线程池已经执行的和未执行的任务总数

通过这些方法,可以对线程池进行监控,在 ThreadPoolExecutor 类中提供了几个空方法,如 beforeExecute 方法, afterExecute 方法和 terminated 方法,可以扩展这些方法在执行前或执行后增加一些新的操作,例如统计线程池的执行任务的时间等,可以继承自 ThreadPoolExecutor 来进行扩展。

方法操作
shutdown()线程池延迟关闭时(等待线程池里的任务都执行完毕),统计已执行任务、正在执行任务、未执行任务数量
shutdownNow()线程池立即关闭时,统计已执行任务、正在执行任务、未执行任务数量
beforeExecute(Thread t, Runnable r)任务执行之前,记录任务开始时间,startTimes这个HashMap以任务的hashCode为key,开始时间为值
afterExecute(Runnable r, Throwable t)任务执行之后,计算任务结束时间。统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止信息