并发编程 - 第二章

87 阅读6分钟

1、线程池

Java中的线程池通常是指ThreadPoolExecutor。学习线程池,首先学习ThreadPoolExecutor的构造。

1.1 ThreadPoolExecutor核心参数

1.png 通过ThreadPoolExecutor的构造方法可以知道线程池的7个核心参数:

(1)corePoolSize:核心线程数量

核心线程数是线程池维护的最小线程数量。这些线程在创建后,除非设置了allowCoreThreadTimeout为true且空闲时间超过了keepAliveTime,否则它们会一直存活在线程池中。

当线程池中的线程数小于核心线程数时,即使存在空闲的核心线程,线程池也会为新的任务创建新的核心线程来执行,不会直接复用现有的空闲核心线程。因为核心线程被设计为活跃线程是为了确保线程池有足够的处理能力来快速响应新任务。

(2)maximumPoolSize:最大线程数

线程池能够容纳同时执行的最大线程数。当工作队列已满,且当前线程数小于maximumPoolSize时,会创建新线程来处理任务。

(3)keepAliveTime:最大空闲时间

通常指的是非核心线程允许空闲的最大时间,如果设置了核心线程允许超时,也会执行该规则。当线程数超过corePoolSize时,空闲时间超过keepAliveTime的线程将被终止,直到线程池中的线程数缩减到corePoolSize。

(4)unit:空闲时间单位

(5)workQueue:工作队列

阻塞队列BlockingQueue,用于存放待执行的任务。任务提交并不总是先进入BlockingQueue再分配线程执行。线程池处理任务的流程如下:

当一个任务被提交给线程池时,线程池首先会判断当前运行的线程数是否小于核心线程数。如果当前线程数小于核心线程数,线程池会新建一个线程来执行任务,而不是将任务放入BlockingQueue。如果当前线程数已经达到或超过核心线程数,但小于最大线程数,并且BlockingQueue未满,线程池会将任务放入BlockingQueue中等待执行。一旦有线程空闲,就会从BlockingQueue中取出任务并执行。如果BlockingQueue已满,且当前线程数小于最大线程数,线程池会创建新的非核心线程来执行任务。如果BlockingQueue已满,且线程数已达到最大线程数,线程池会使用配置的拒绝策略来处理新提交的任务,而不是继续将任务放入BlockingQueue或创建新线程。

(6)threadFactory: 线程工厂

用于创建新线程的ThreadFactory。如果不指定,则使用Executors.defaultThreadFactory()作为默认线程工厂。

(7)Handler:拒绝策略 AbortPolicy:不执行此任务,而且直接抛出一个运行时异常; 2.png DiscardPolicy:新任务被提交后直接被丢弃掉,并且不会抛出异常,无法感知到这个任务会被丢弃,可能造成数据丢失;
3.png DiscardOldestPolicy:丢弃工作队列中的头节点,通常是存活时间最长并且未被处理的任务,然后尝试执行当前提交的任务; 4.png CallerRunsPolicy:新任务提交后,如果线程池没被关闭且没有能力执行,由调用线程执行; 5.png Java提供的四种默认策略需要在不同场景下使用,这几种策略都有可能导致,异常未被正确处理或者发现,进而引起数据丢失或者数据不一致。因此也可以实现RejectedExecutionHandler接口自定义拒绝策略,满足定制化需求。可以考虑以下建议:

①捕获并处理异常:确保在提交任务到线程池的代码块中捕获RejectedExecutionException,并适当地处理它。可以记录错误、通知用户、或者尝试使用其他策略(如重试、降级处理)来执行被拒绝的任务。

②优雅降级:在任务被拒绝时,考虑实现一种降级处理逻辑。例如,对于非关键性任务,可以在任务被拒绝时返回默认值、缓存结果或执行简化版本的逻辑。这样可以确保应用程序在面临压力时仍然能够保持一定的可用性。

③监控和警报:设置监控机制来跟踪线程池的状态和任务拒绝的频率。当任务拒绝率超过某个阈值时,可以触发警报,以便及时采取行动,如增加线程池大小、优化任务处理逻辑或调整系统负载。

④调整线程池配置:根据应用程序的负载特性和性能要求,合理设置线程池的大小、队列容量和拒绝策略。例如,如果任务量很大且需要快速响应,可以考虑增加线程池的大小;如果任务量不稳定或具有周期性高峰,可以使用动态调整线程池大小的策略。

⑤任务优先级和分类:对于不同类型的任务,可以根据其重要性和紧急性设置不同的优先级和分类。对于高优先级或紧急任务,可以使用单独的线程池或队列来处理,以确保它们得到及时处理。

⑥重试机制:对于可能被拒绝的任务,可以实现一种重试机制。当任务被拒绝时,可以将其重新放入队列中等待再次处理,或者延迟一段时间后再次提交。重试机制可能会增加系统的复杂性和开销,需要谨慎使用。

⑦任务备份和恢复:对于关键任务,如果可能的话,考虑实现一种任务备份机制。当任务被丢弃时,可以将任务信息保存到持久化存储中,并在系统负载降低时尝试重新提交这些任务。

1.2 常用的线程池

(1)FixedThreadPool

特点:核心线程数和最大线程数相同,线程数量固定。

使用场景:适用于任务数量基本固定,且任务执行时间较长的场景。 6.png (2)CachedThreadPool

特点:线程数几乎可以无限增加,理论上最大值为Integer.MAX_VALUE,当线程闲置时还可以进行回收。

使用场景:适用于任务数量频繁变化,且任务执行时间较短的场景。 7.png (3)ScheduledThreadPool

特点:支持定时或周期性执行任务。

使用场景:需要按照一定时间间隔或固定时间延迟后执行任务的场景。 8.png (4)SingleThreadExecutor

特点:线程池中只有一个线程执行任务,保证任务执行的有序性。

使用场景:适用于需要按照特定顺序执行任务,且不希望多个任务并发执行的场景。 9.png (5)SingleThreadScheduledExecutor

特点:与ScheduledThreadPool相似,但内部只有一个线程。

使用场景:与SingleThreadExecutor相似,但需要定时或周期性执行任务的场景。 10.png (6)ForkJoinPool

特点:支持将一个任务拆分成多个“小任务”并行计算,特别适用于递归调用的分治算法。

使用场景:需要进行大量计算,且计算任务可以拆分为多个独立子任务的场景。 11.png

1.3 Executors 和ExecutorService

Executors类是一个线程池的工具类,它提供了创建各种类型线程池的静态工厂方法。ExecutorService线程池的顶级接口,提供了线程池执行任务的基本方法。 12.png 13.png 14.png 15.png