Java并发(六)Java线程池和Executor框架

2024 年 5 月 29 日 星期三(已编辑)
/
58
2
这篇文章上次修改于 2024 年 7 月 5 日 星期五,可能部分内容已经不适用,如有疑问可询问作者。

Java并发(六)Java线程池和Executor框架

什么是线程池?为什么要用线程池?

线程池是一种线程管理机制,它的主要作用是:

  1. 复用线程资源: 线程池会预创建一定数量的线程,并把这些线程保存在一个池子里,当需要执行任务时,可以直接复用池中的线程,避免频繁创建和销毁线程的开销
  2. 控制并发线程数: 线程池可以限制并发线程的数量,防止因为创建大量线程而导致系统资源耗尽的问题
  3. 任务排队和调度: 当所有线程都处于繁忙状态时,新来的任务会被放入一个任务队列中排队,等待有空闲线程时再执行。线程池还可以根据任务的优先级来调度执行顺序

几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

线程池实现原理

  • 首先会判断核心线程池是否已满:如果没有满,则直接创建新线程来执行任务。如果已经满了,尝试将任务放入队列
  • 判断队列是否已经满了:如果没有满,则任务放入队列,结束。如果队列已经满了,则进行下一步判断
  • 判断线程池是否已经满:如果没满,则创建新线程来执行任务。如果已经满了,则按照饱和策略来处理任务

核心线程池相当于正式员工,要工作多的积压了,就找外包(线程池)做,做完空闲下来就辞了

线程池核心参数有哪些

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程池大小
  • maximumPoolSize : 线程池最大线程数
  • workQueue: 阻塞队列,新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中

ThreadPoolExecutor其他常见参数 :

  • keepAliveTime:空闲线程存活时间
  • unit : keepAliveTime 参数的时间单位
  • threadFactory :executor 创建新线程的工程类
  • handler :拒绝策略(饱和策略)

如果线程池当前处于空闲的状态,核心线程数量是不会被销毁的,那这几个核心线程处于什么状态?为什么处于这个状态?

首先线程本身创建和销毁都是成本比较高的,那就排除new和terminated状态,没有任务运行排除runnable状态,剩下阻塞和等待,因为线程不会销毁需要一直等待执行任务,超时等待也不太可能,最后同步锁才会进入阻塞状态,所以是一直等待

常用线程池的区别和特点

  • newCachedThreadPool

    • 特点:newCachedThreadPoolt创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时,它可以灵活的添加新的线程,而不会对池的长度作任何限制

    • 缺点:他虽然可以无限的新建线程,但是容易造成堆内存谥出,因为它的最大值是在初始化的时候设置为Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值

  • newFixedThreadPool

    • 特点:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。
    • 缺点:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间)
  • newScheduledThreadPool
    • 特点:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于Timer(Timer是Java的一个定时器类)
    • 缺点:同样使用无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间)
  • newSingleThreadExecutor
    • 特点:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
    • 缺点:缺点的话,很明显,他是单线程的,高并发业务下有点无力

ThreadPoolExecutor饱和策略有哪些?

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用提交任务的线程运行任务(A线程提交任务,A负责运行)。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你不能丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。

Java中executor和executors的区别

  • Executor是一个接口,定义了任务执行的通用方法。
  • Executors是一个工厂类,提供了创建和配置各种类型Executor实现的方法。

在实际使用中,我们通常会使用Executors工厂方法创建所需的Executor实现,然后使用Executor接口的execute()方法来执行任务。这种方式可以简化任务执行的逻辑,同时也可以根据需求选择合适的线程池实现。

线程池都有哪些状态

  • RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
  • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
  • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
  • TIDYING:所有的任务都销毁了,workerCount为O,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()。
  • TERMINATED:terminated(O方法结束后,线程池的状态就会变成这个。

线程池中submit()和execute()方法有什么区别?

  • 相同点:相同点就是都可以开启线程执行池中的任务。

  • 不同点:

    • 接收参数:execute()只能执行Runnable类型的任务。submit()可以执行Runnable和Callable类型的任务

    • 返回值:submit()方法可以返回持有计算结果的Future对象(表示一个异步计算的结果),而execute()没有

    • 异常处理:

      • execute()提交到线程池并在执行过程中抛出异常时,如果这个异常没有在任务内被捕获,那么该异常会导致当前线程终止
      • 对于通过submit()提交的任务,如果在任务执行中发生异常,这个异常不会直接打印出来,异常会被封装在由submit()返回的Future对象中。当调用Future.get()方法时,可以捕获到一个ExecutionException。在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务

如何合理分配线程池大小?

要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据CPU密集和IO密集来分配。

  • CPU密集型时,任务可以少配置线程数,大概和机器的CPU核数相当,这样可以使得每个线程都在执行任务
  • IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*CPU核数

总结:

  • 线程等待时间比CPU执行时间比例越高,需要越多线程。
  • 线程CPU执行时间比等待时间比例越高,需要越少线程。

当然,实际应用中没有固定的公式,需要结合测试和监控来进行调整。

线程池如何实现动态修改?

  1. 首先线程池提供了部分setter方法可以设置线程池的参数:

    • 修改核心线程数,最大线程数,空闲线程停留时间,拒绝策略等。

    • 可以将线程池的配置参数放入配置中心,当需要调整的时候,去配置中心修改就行。

  2. 什么时候修改呢?

    • 这里需要监控报警策略,获取线程池状态指标,当指标判定为异常之后进行报警
    • 分析指标异常原因,评估处理策略,最后通过上述线程池提供的接口进行动态修改。(可以将动态配置)

使用无界队列的线程池会导致什么问题?

例如newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM。

线程池调优

  • CPU密集型任务配置尽可能小的线程,cpu核数+1。
  • IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*cu核数。
  • 混合型任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务。只要这两个任务执行的时间相差不是太大,那么分解后并发执行的吞吐率要高于串行执行的吞吐率;如果这两个 任务执行时间相差太大,则没必要进行分解。
  • 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先得到执行。
  • 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,线程数应该设置得较大,这样才能更好的利用CPU。
  • 建议使用有界队列,有界队列能增加系统的稳定性和预警能力。可以根据需要设大一点,比如几千。使用无界队列,线程池的队列就会越来越大,有可能会撑满内存,导致整个系统不可用。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...