线程池【开发实践】

一、为什么要用线程池

1.1 单线程的问题

单线程无法利用CPU的多核资源,且存在阻塞问题。使用单个线程来处理一组任务,若当前任务阻塞了线程,该线程将失去CPU的使用权,而不是去执行其他可以执行的任务。

1.2 手动创建多线程的问题

需要开发人员去管理线程的生命周期,麻烦,且可能出错。
还存在并发数不可控的问题,如每次到达一个任务时都新建一个线程来处理,当大量任务到达时,会创建大量的线程,导致线程间的竞争加剧,且可能导致系统资源耗尽。

1.3 线程池的作用(优点)

  • 节省线程创建和销毁的开销:线程池通过复用一组线程,可以节省频繁创建和销毁线程的开销。
  • 控制并发数:线程池可以控制最大并发数,避免线程过多导致系统资源耗尽。
  • 提高响应速度:当任务到达时,可以直接使用线程池中已经创建好的线程,节省了新建线程的时间。
  • 管理线程:线程池负责内部线程的生命周期管理,包括创建、销毁和调度等,无需人工干预。

1.4 线程池的使用场景

  • 处理并发请求:每个请求都要一个线程来处理,使用线程池来复用一组线程,能节省创建和销毁线程的开销。
  • 执行异步任务:避免耗时操作(如邮件发送、文件上传)阻塞主线程,可以使用线程池来异步执行这些任务。
  • 处理IO密集型任务:线程在进行IO操作时会阻塞,使用线程池来调度这些任务,可以在任务阻塞时调度其他任务继续执行,避免CPU空闲。
  • 处理计算密集型任务:使用线程池来执行计算密集型任务,可以充分利用CPU的多核资源,也能控制并发数量避免系统资源耗尽。
  • 后台服务:利用线程池来执行后台服务,与前台线程的执行独立,互不影响。
  • 执行定时任务与周期性任务:可以使用ScheduledThreadPool在后台执行这些任务,与前台线程的执行独立。

二、线程池的基础知识

2.1 线程池的核心组件

  • 线程池管理器:负责线程池的创建、销毁、管理和配置。
  • 工作线程:工作线程是线程池中的实际工作者,负责执行提交给线程池的任务。
  • 任务接口:每个提交到线程池的任务都需要实现一个任务接口。Runnable用于定义无需返回值的任务,而Callable则用于定义可以有返回值的任务,并且可以抛出异常。
  • 工作队列(任务队列):用于暂存等待被执行的任务,有多种实现方式,未必是先进先出。
  • 线程工厂:线程工厂的作用是定义一个规范来创建线程,允许在创建线程时进行定制化设置,例如设置线程的名称、优先级、是否为守护线程(daemon thread)等属性。
  • 拒绝策略:见后。
    (后三个核心组件是线程池的后三个配置参数)

2.2 JUC中的线程池架构

Executor
是线程池管理器的顶层接口,只定义了一个方法void execute(Runnable command),用来提交Runnable任务。

ExecutorService
该接口扩展了Executor,增加了更多管理线程池的方法,如提交Callable任务、提交批量任务、线程池的生命周期管理(shutdown()shutdownNow())、获取线程池状态等。

AbstractExecutorService实现了该接口中的部分方法,为创建自定义的线程池服务实现提供了基础框架。

ThreadPoolExecutor继承自AbstractExecutorService,是JUC中线程池管理器的核心实现类。

Executors是一个静态工厂类,提供了多种线程池管理器。

ScheduledExecutorService
该接口扩展了ExecutorService,增加了定时执行和周期性执行任务的功能。ScheduledThreadPoolExecutor实现该接口,专为计划任务而设计。
在这里插入图片描述

2.3 线程池的配置参数

  • corePoolSize:核心线程数。
  • maximumPoolSize:最大线程数。
  • keepAliveTime:空闲线程存活时间。当线程池中的工作线程数量超过核心线程数时,额外的工作线程在空闲这个时间段后会被终止,直到工作线程数量降到核心线程数。但如果设置了allowCoreThreadTimeOuttrue,那么核心线程也可以被终止。
  • unit:空闲线程存活时间的单位,可以用枚举类TimeUnit设置。
  • workQueue:工作队列。
  • threadFactory:线程工厂。
  • handler:拒绝策略。当阻塞队列已满且工作线程数大于等于最大线程数时,无法接受新的任务,会按照拒绝策略进行处理。

2.4 线程池常见的拒绝策略(可自定义)

  • AbortPolicy:拒绝策略。是默认策略,会将新任务拒绝,并且抛出RejectedExecutionException异常。
  • DiscardPolicy:抛弃策略。会将新任务丢掉,但不会跑出异常。
  • DiscardOldestPolicy:抛弃最老任务策略。不是丢弃新任务,而是先丢弃最先进入队列的任务,然后将新任务入队。
  • CallerRunsPolicy:调用者执行策略。让提交任务的线程去执行新任务,而不是使用线程池中的线程去执行。
    在这里插入图片描述

2.5 线程池的生命周期

  • New:新建状态。当线程池创建后,但尚未开始执行任务时,它处于新建状态。此时,线程池中的线程尚未启动。
  • Running:运行状态。新建的线程池开始接受并处理任务后就会进入运行状态,这是线程池的正常工作状态,可以接收新任务和处理工作队列中的任务。
  • Shutdown:关闭状态。调用shutdown()方法后,线程池会进入关闭状态。此时,线程池不再接受新任务,但不会立刻终止,它会继续处理队列中已有的任务直到所有任务完成。
  • Stop:停止状态。调用shutdownNow()方法后,线程池会进入停止状态,此时会中断正在执行任务的线程,清空任务队列,并返回未开始执行的任务列表。
  • Tidying:整理状态。完成关闭状态或停止状态的工作后,线程池会进入整理状态。此阶段,线程池中的线程数量降为0,即将调用terminated()钩子方法。
  • Terminated:终止状态。在整理状态后,线程池调用terminated()方法并进入终止状态。此时线程池生命周期结束,会释放所有资源。
    在这里插入图片描述

2.6 线程池的任务调度规则

当向线程池提交一个任务时:

  1. 若线程池中的工作线程数量小于核心线程数,执行器总是优先创建一个新的工作线程来执行任务,而不是使用一个空闲的工作线程(目的是快速让线程池中有足够的活跃线程)。若工作线程数量大于等于核心线程数,则根据工作队列的情况进行相应的处理。
  2. 若工作队列未满,则将任务入队。若工作队列已满,则根据工作线程数量与最大线程数的关系进行相应的处理。
  3. 若工作线程数大于等于最大线程数,则执行拒绝策略。否则,会新建一个非核心线程来立即执行新的任务。
    在这里插入图片描述

2.7 线程池执行器的钩子方法

beforeExecute(Thread t, Runnable r): 任务执行之前的钩子方法。
afterExecute(Runnable r, Throwable t): 任务执行之后的钩子方法。
terminated(): 线程池终止时的钩子方法。

三、使用基础

3.1 常见的工作队列

工作队列基于阻塞队列实现,我们可以实现BlockingQueue来自定义阻塞队列,也可以使用JUC中提供的阻塞队列:

  • ArrayBlockingQueue:基于数组的有界阻塞队列。队列大小是固定的,在创建时必须指定。由于是基于数组,所以访问速度快,但可能有扩容受限的问题。
  • LinkedBlockingQueue:基于链表的阻塞队列。如果不指定容量,则默认为Integer.MAX_VALUE。相比ArrayBlockingQueue,插入和删除操作可能稍微慢一点,但由于链表的动态性,它可以更灵活地调整大小。
  • PriorityBlockingQueue:基于最小堆的无界的优先级队列,元素按照自然排序或提供的比较器进行排序。任务按照优先级顺序被处理,而非先进先出。
  • DelayQueue:基于PriorityBlockingQueue的无界阻塞队列,其中元素只有在延迟期满后才能被获取,否则将阻塞等待,常用于实现定时任务和延迟操作。
  • SynchronousQueue:一个特殊的队列,它没有内部容量,每个插入操作必须等待另一个线程的对应移除操作,反之亦然。适用于直接的生产者-消费者传递,非常适合传递性操作。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,支持公平和非公平模式。它还提供了一个tryTransfer()方法,允许生产者直接将元素转移给等待中的消费者,如果没有消费者等待,则可以选择是否阻塞(生产者可阻塞)。
  • LinkedBlockingDeque:双向链表实现的双向阻塞队列,既可以用作栈,也可以用作队列。它支持有限或无界的队列,并提供了putFirst()putLast()等方法来控制元素的插入位置。

3.2 Executors提供的线程池

在这里插入图片描述

3.3 使用Executors创建线程的问题

  • 定长线程池/单线程话线程池:阻塞队列无界,可能导致JVM出现OOM(Out Of Memory)异常。
  • 定时线程池/可缓存线程池:最大线程数量不设限上,如果任务提交较多,就会造成大量的线程被启动,可能造成OOM异常,也可能导致CPU线程资源耗尽。

3.4 线程池的使用规范

  • 禁止嵌套使用线程池:同一个调用链路中,父子任务共用同一个线程池,容易发生线程嵌套现象,增加线程死锁风险。
  • 核心业务场景必须使用独立线程池:不同业务使用相同线程池,当部分业务吞吐较大或性能劣化时,容易因资源争用影响到其他业务的稳定性。
  • 设置等待超时时间(如get结果):避免长时间阻塞。
  • 慎用AbortPolicy之外的拒绝策略:DiscardPolicyDiscardOldestPolicy丢弃任务时无感知(没抛出异常),存在风险。CallerRunsPolicy使用主线程执行任务,当此拒绝策略被大量触发时,容易因执行时间拉长引起阻塞导致事故。

四、开发实践

4.1 线程数量配置

IO密集型
核心线程数设置得比CPU核心数稍大,因为当线程在等待I/O时,CPU可以调度其他线程执行。最大线程数可以设置得更高,甚至远大于CPU核心数,具体数值取决于系统的I/O能力及预期的并发水平。一般推荐设置为2 * CPU核心数或更高,但需注意不要设置得过高以免过度消耗系统资源。
计算密集型
核心线程数和最大线程数通常设置为CPU核心数,因为在这种情况下,更多的线程并不能带来性能提升,反而会因为上下文切换带来额外开销。

4.2 提交任务:以submit(callable)为例

	 // 创建线程池
	ExecutorService executor = Executors.newSingleThreadExecutor();

	// 创建Callable任务对象
	Callable<Result> task = () -> {
		// ...
	};

	// 提交任务并获取Future对象
    Future<Result> future = executor.submit(task); // 提交任务并获取Future

    // 获取结果
    try {
    	// get()方法会阻塞,如果任务执行过程中发生了异常或超时,那么此处会抛出该异常
        Result res = future.get(timeout);
    } catch (InterruptedException e) {
    	// 处理中断异常...
    } catch (ExecutionException e) {
        // 处理执行异常...
    }
	
	// 关闭ExecutorService
    executor.shutdown();

4.3 关闭线程池

调用shutdown()shutdownNow()后,当前线程不会等待线程池的关闭,若当前线程结束了,可能导致内存泄露和资源浪费。

若线程池中还有活动的线程,就算主线程结束了,JVM也会因为存在活跃的非守护线程而无法退出,导致应用程序无法正常退出。

awaitTermination()方法用来阻塞当前线程,直到线程池中的所有任务完成执行或者超时,或者当前线程被中断。可以用于等待线程池中的线程全部执行完后再退出当前线程,线程池中的所有线程在超时前执行完毕,会返回true并恢复当前线程的执行。

ExecutorService executorService = Executors.newFixedThreadPool(10);

// ... 提交任务到线程池

// 关闭线程池
executorService.shutdown();
try {
    // 等待一定时间,直到超时或者线程池中的线程全部执行结束
    if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
        // 如果超时,则尝试强制关闭
        executorService.shutdownNow();
        // 等待一定时间,直到超时或者线程池中的线程全部执行结束
        if (!executorService.awaitTermination(60, TimeUnit.SECONDS))
            System.err.println("线程池未在规定时间内停止!");
    }
} catch (InterruptedException ie) {
    // 强制终止线程池
    executorService.shutdownNow();
    // 保持当前线程的中断状态
    Thread.currentThread().interrupt();
}

4.4 动态线程池

动态线程池在标准线程池的基础上增加了动态调整线程池参数的能力,以更好地适应运行时变化的业务需求和资源状况。这种线程池能够根据当前系统的负载、任务队列的长度、执行时间等多种因素,在不重启应用程序的情况下,自动或手动地调整线程池的核心参数,比如核心线程数、最大线程数、队列容量等,从而达到优化系统性能、资源利用率和响应时间的目的。

动态线程池除了提供动态配置功能以外,一般还提供了监控与告警功能。

参考

Java线程池(超详细)
Java 多线程:彻底搞懂线程池

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值