线程池的创建
使用Executors
创建线程池:
1 | ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单个线程 |
但是不建议使用Executors
创建线程池,在阿里巴巴技术规范中写有:
使用
Executors
创建线程池的本质实际上也是调用了ThreadPoolExecutor
创建线程池,但是创建的线程池默认配置不太合理?
TreadPoolExector
ThreadPoolExecutor
的构造方法:
1 | public ThreadPoolExecutor(int corePoolSize, |
ThreadPoolExecutor
的构造函数具有以下七个参数,它们分别是:
corePoolSize
(核心线程数):它指定了线程池中保持活动状态的核心线程数量。核心线程是一直存活的线程,即使它们处于空闲状态也不会被回收。当有新任务提交时,核心线程会立即执行任务。如果使用的是无界队列,线程池中的线程数永远不会超过核心线程数。
maximumPoolSize
(最大线程数):它定义了线程池中允许创建的最大线程数量。当工作队列已满且当前线程数小于最大线程数时,线程池会创建新的线程来执行任务。如果使用的是有界队列,当队列已满且线程数达到最大值时,新的任务会触发拒绝策略。
keepAliveTime
(线程空闲超时时间):它表示非核心线程空闲的最大时间。当线程池中的线程数超过核心线程数,并且空闲时间超过该值时,多余的线程会被终止并从线程池中移除,以减少资源消耗。新任务到达时,如果线程池中的线程数小于核心线程数,可能会重新创建线程。
unit
(空闲超时时间的单位):用于指定
keepAliveTime
参数的单位,可以是TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
等。workQueue
(工作队列):它定义了用于保存待执行任务的阻塞队列。当任务提交到线程池时,如果线程数小于核心线程数,会创建新线程来执行任务。如果线程数达到核心线程数,而工作队列未满,则将任务放入队列中等待执行。工作队列可以是有界队列(如
ArrayBlockingQueue
)或无界队列(如LinkedBlockingQueue
)。threadFactory
(线程工厂,可选):线程工厂指定创建线程的方式,用于创建新线程的工厂对象。线程工厂可以根据需要对线程进行自定义配置,例如设置线程名字、设置线程优先级等。如果未指定,将使用默认的
DefaultThreadFactory
handler
(拒绝策略,可选):它定义了当线程池无法接受新任务时的处理方式。拒绝策略可以是预定义的几种策略,如抛出异常、丢弃任务、阻塞等。也可以根据需要自定义拒绝策略实现
RejectedExecutionHandler
接口。
我们可以这样类比:
注意:核心线程没有空闲时,超量任务首先放入队列中,队列满了才会开普通线程处理!
如果corePoolSize
为0,则要使用SynchronousQueue
避免无限阻塞。因为核心线程为0时,任务会先放入队列中,队列放不下了再使用普通线程,如果队列是无限的就会导致一直阻塞
工作队列 BlockQueue
使用用ThreadPoolExecutor
需要指定一个BlockingQueue
任务等待队列。
BlockingQueue(阻塞队列)定义了一组用于插入、获取和检查元素的方法。与普通的队列不同,BlockingQueue在队列为空时,获取元素的操作会被阻塞,直到队列中有可用元素为止。同样地,当队列已满时,插入元素的操作也会被阻塞,直到队列有空闲空间为止。
BlockQueue有四组API:
BlockingQueue提供了多个实现类,其中常用的有以下几种:
ArrayBlockingQueue
:基于数组实现的有界阻塞队列。它在构造时需要指定队列的容量,并且在队列已满时会阻塞插入操作,直到有空闲空间。LinkedBlockingQueue
:基于链表实现的可选有界或无界阻塞队列。如果构造时不指定容量,则队列大小默认为无限制。PriorityBlockingQueue
:基于优先级堆实现的无界阻塞队列。元素按照优先级进行排序,可以自定义比较器。SynchronousQueue
:同步队列。是一个没有容量的阻塞队列,任何一次插入操作的元素都要等待相对的删除/读取操作,否则进行行行插入操作的线程就要一直等待,反之亦然- 进去一个元素,必须等待取出来之后,才能再往里面放入一个元素
- 使用lock锁保证线程安全的
DelayQueue
:基于优先级堆实现的延迟阻塞队列。其中的元素必须实现Delayed
接口,只有经过一定时间后才能被取出。
BlockingQueue实现原理:ReentrantLock + Condition
ArrayBlockingQueue
和LinkedBlockingQueue
区别:
- ArrayBlockingQueue
使用数组作缓冲区,有界,生产者消费者之间使用独占锁,即生产者和消费者共用一把缓冲区的锁,出队和入队不能同时进行
- LinkedBlockingQueue
使用链表作缓冲区,无界,生产者消费者之间使用分离锁,即生产者和消费者分别用一把缓冲区的锁,出队和入队可以同时进行
拒绝策略
如上面介绍,拒绝策略需要实现RejectedExecutionHandler
接口,Executors
为我们提供了4种拒绝策略:
AbortPolicy
(默认):丢弃任务并抛出RejectedExecutionException
异常CallerRunsPolicy
:直接运行这个任务的run方法,但并非是由线程池处理,而是交由任务的调用线程处理DiscardPolicy
:直接丢弃任务,不抛出任何异常DiscardOldestPolicy
:将当前处于等待队列列头的等待任务强行取出,然后再试图将当前被拒绝的任务提交
常见线程池种类
四种线程池的特点:
SingleThreadExecutor
单线程线程池- 特点:线程池只有一个线程
- 使用一个
LinkedBlockingQueue
作为工作队列,未指定容量(默认值Integer.MAX_VALUE
)
FixedThreadPool
固定线程池- 特点:最大线程数就是核心线程数,即只有核心线程
keepAliveTime=0
,但核心线程不会被回收或者销毁- 无界队列:使用
LinkedBlockingQueue
作为工作队列,未指定容量(默认值Integer.MAX_VALUE
) - 适用于需要有一定持续并发量的场景
CachedThreadPool
缓存线程池- 没有核心线程,普通线程数量无限
keepAliveTime=60
,线程闲置60s后回收- 使用
SynchronousQueue
作为工作队列,它不会保存任务,而是直接将任务交给空闲线程执行 - 处理大量短时间工作任务的线程池,适用于项目中多线程的场景不多或者是需要快速响应的场景,即来即处理,且用完释放,不占用过多资源
ScheduledThreadPool
定时线程池- 指定核心线程数量,普通线程数量无限
- 任务队列为延时阻塞队列
DelayQueue
- 适用用于执行定时或周期性的任务
线程池调优
在设置线程池大小时,可以考虑任务类型和系统资源的特点,以确定适当的线程池大小。具体而言,对于 CPU 密集型任务和 I/O 密集型任务,可以采取以下建议:
- CPU 密集型任务
- 对于 CPU 密集型任务,线程数应与 CPU 核心数相近或稍多一些,以充分利用 CPU 资源,并避免过多的线程竞争和上下文切换开销。
- 可以通过
Runtime.getRuntime().availableProcessors()
获取当前系统的 CPU 核心数,作为线程池的核心线程数。 - 由于 CPU 密集型任务不涉及阻塞等待,可以选择较小的工作队列容量或使用
SynchronousQueue
,使得任务提交后立即执行。
- I/O 密集型任务
- 对于 I/O 密集型任务,一般建议将线程数设置为 CPU 核心数的几倍,例如 2 倍或 4 倍,以充分利用 I/O 操作的等待时间,提高系统的吞吐量。
- 由于 I/O 操作会涉及到阻塞等待,可以设置较大的工作队列容量,以处理可能的任务积压。
实际线程池大小的选择还取决于任务的具体特点和系统资源的限制。在实际应用中,可以通过测试和性能调优来确定最佳的线程池大小,确保任务能够高效执行并充分利用系统资源。