文章目录
- 引言
- 概念
- 优势
- Java 中的线程池实现
- 线程池的核心参数
- 1. corePoolSize:核心线程数
- 2. maximumPoolSize:最大线程数
- 3. keepAliveTime:线程空闲时间
- 4. unit:时间单位
- 5. workQueue:任务队列
- 6. threadFactory:线程工厂
- 7. handler:拒绝策略
- 创建线程池的方式
- 1. 固定大小的线程池FixedThreadPool
- 2. 缓存线程池CachedThreadPool
- 3. 单线程线程池SingleThreadExecutor
- 4. 定时线程池ScheduledThreadPool
- 5. 自定义线程池 ThreadPoolExecutor
- 强制规定
- 结论
引言
在现代软件开发中,多线程编程技术被广泛应用于提高应用程序的性能和响应速度。Java 语言提供了丰富的多线程支持,其中线程池是一种非常重要的机制,用于管理和重用线程,从而减少线程创建和销毁带来的开销。本文将详细介绍 Java 中的线程池概念、工作原理以及如何在实际项目中有效使用线程池。
概念
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在线程池中复用已创建的线程。通过这种方式,线程池可以控制运行的线程数量,处理过程中,负责在运行完毕后重新添加线程到线程池中,以供后续新任务使用。
优势
- 减少创建和销毁线程的开销:线程的创建和销毁都是昂贵的操作,尤其是在频繁创建和销毁线程的情况下。线程池通过复用已存在的线程减少了这种开销。
- 提高响应速度:当任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以有效地控制运行的线程数量,合理利用系统资源。
- 提供更强大的功能:如定时执行、定期执行、线程中断等。
Java 中的线程池实现
Java 提供了 java.util.concurrent 包来支持线程池的创建和管理。其中,ExecutorService 接口是线程池的主要接口,ThreadPoolExecutor 类则是 ExecutorService 的一个实现,提供了更加灵活的线程池配置选项。
线程池的核心参数
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
1. corePoolSize:核心线程数
线程池中保持的最小线程数,即使这些线程处于空闲状态也不会被回收。
通常可以按照任务类型参考如下方式设置核心线程数:
- CPU密集型任务:选择较小的线程池大小,推荐
核心线程数 = CPU核心数 + 1
。 - I/O密集型任务:选择较大的线程池大小,因为I/O操作会导致线程阻塞,需要更多的线程来保持CPU的利用率。推荐
核心线程数 = CPU核心数 * 2
。
实际项目中,可以基于这些参数进行初始化,然后再进行严格测试后确定最终参数。
- 如果当前线程数小于corePoolSize,即便其他工作线程是空闲的,也会创建新的线程来处理新提交的任务
- 当线程数大于等于corePoolSize且小于maximumPoolSize时,只有当工作队列满了之后才会创建新的线程。
- 如果设置了allowCoreThreadTimeOut(true),则核心线程也会在空闲时间超过keepAliveTime后被终止。
2. maximumPoolSize:最大线程数
线程池中允许的最大线程数。
核心线程数和最大线程数都是线程池的重要参数,它们都可以动态设置,但是需要遵循一些规则:
- 动态设置核心线程数可能会影响线程池的整体性能和稳定性,因此应该谨慎操作。
- 在已经提交了一些任务的情况下,如果减小核心线程数,可能导致已提交任务无法处理。
- 最大线程数不能小于核心线程数。
- 增大最大线程数可能会对系统资源产生压力,应该慎重考虑。
- 当线程数已经达到maximumPoolSize且工作队列已满时,线程池会拒绝新任务,此时会触发拒绝策略。
- 合理设置maximumPoolSize可以防止系统资源耗尽,特别是在处理大量并发任务时。
3. keepAliveTime:线程空闲时间
线程池中超过corePoolSize的空闲线程等待新任务的最长时间。
- 如果线程空闲时间超过了keepAliveTime,则这些线程会被终止,直到线程数等于corePoolSize。
- 设置合理的keepAliveTime可以平衡资源利用率和响应速度。
4. unit:时间单位
keepAliveTime 的时间单位。
常见的时间单位包括TimeUnit.SECONDS、TimeUnit.MINUTES、TimeUnit.HOURS等。
5. workQueue:任务队列
用于保存等待执行的任务的阻塞队列。
常见类型
- LinkedBlockingQueue:基于链表结构的阻塞队列,吞吐量通常要高于ArrayBlockingQueue。
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,先进先出(FIFO)。
- SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待另一个线程的对应移除操作。
- PriorityBlockingQueue:具有优先级的无界阻塞队列。
选择合适的任务队列类型对线程池的性能影响很大。
有界队列可以防止资源耗尽,但可能导致任务被拒绝;无界队列可以无限接收任务,但可能占用大量内存。
6. threadFactory:线程工厂
用于创建新线程的工厂。
《阿里巴巴Java开发手册》中有一个强制规定:创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
- 可以通过自定义ThreadFactory来设置线程的名称、优先级等属性。
- 默认的ThreadFactory会创建一个默认优先级的线程。
7. handler:拒绝策略
当线程池和任务队列都满时,用于处理新提交任务的策略。
常见策略
- AbortPolicy:默认策略,抛出RejectExecutionException异常。
- CallerRunsPolicy:由调用线程(提交任务的线程)执行该任务,不会丢弃任务。
- DiscardPolicy:直接丢弃任务,不抛出异常。
- DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试重新提交新任务。
- 选择合适的拒绝策略可以避免任务丢失或系统崩溃。
- 在生产环境中,通常建议使用CallerRunsPolicy或自定义拒绝策略来处理任务。
创建线程池的方式
1. 固定大小的线程池FixedThreadPool
创建方式:Executors.newFixedThreadPool(int nThreads)
特点
- 线程池大小固定为nThreads
- 如果所有线程都在忙,新的任务会被放入队列中等待。
- 适用于负载较重、处理时间较长的任务。
适用场景:适用于需要控制并发数的应用,比如WEB服务器处理请求。
2. 缓存线程池CachedThreadPool
创建方式:Executors.newCachedThreadPool()
特点
- 线程池的大小没有限制,可以根据需要创建新的线程
- 空闲线程等待60s,之后会被回收
适用场景:适用于执行大量耗时短的任务,如I/O操作、网络请求等。
3. 单线程线程池SingleThreadExecutor
创建方式:Executors.newSingleThreadExecutor()
特点
- 只有一个线程来执行任务,保证所有任务按顺序执行
适用场景:适用于需要保证任务顺序的场景,如日志记录、文件写入等。
4. 定时线程池ScheduledThreadPool
创建方式:Executors.newScheduledThreadPool(int corePoolSize)
特点
- 支持定时和周期性任务的执行
- 可以指定延迟执行任务或定期执行任务
适用场景:适用于需要定时执行或周期性执行任务的场景,如定时备份、定时清理等。
5. 自定义线程池 ThreadPoolExecutor
创建方式:直接使用 ThreadPoolExecutor 构造函数
特点
- 提供了更多的配置选项,如核心线程数、最大线程数、线程空闲时间、任务队列等。
- 可以根据具体需求进行灵活配置。
适用场景:适用于需要高度定制化线程池的场景,如高性能服务器、复杂业务逻辑等。
强制规定
《阿里巴巴Java开发手册》有一个强制规定:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors返回的线程池对象的弊端如下:
- FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- CachedThreadPool和ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
结论
线程池是 Java 多线程编程中的一个重要组成部分,合理地使用线程池不仅可以提高应用程序的性能,还能增强系统的稳定性和可靠性。开发者应该根据实际应用场景选择合适的线程池类型,并正确配置相关参数,以达到最佳效果。随着多核处理器的普及,掌握线程池的使用技巧对于现代软件开发而言变得越来越重要。