避免资源浪费的方法
- 合理配置线程池参数
- 核心线程数(corePoolSize):设置为能够满足应用程序常规负载下所需的线程数量。例如,如果应用程序通常会同时处理5个串行任务,并且每个任务执行时间较长,那么可以将
corePoolSize
设置为5。这样,线程池会始终保留这5个线程,避免频繁创建新线程。
- 最大线程数(maximumPoolSize):在考虑到突发负载的情况下,设置合理的最大线程数。比如,应用程序偶尔可能会有10个串行任务同时到达,那么
maximumPoolSize
可以设置为10。注意,这个值不宜过大,否则可能会导致过多线程竞争资源,反而降低性能。
- 存活时间(keepAliveTime):对于非核心线程,如果在
keepAliveTime
时间内没有任务可执行,就会被销毁。在串行任务场景下,如果希望线程在任务执行完后不立即销毁,而是等待后续可能的串行任务,可以将keepAliveTime
设置为一个较大的值,例如Long.MAX_VALUE
。同时,需要将allowCoreThreadTimeOut
设置为true
,这样核心线程在空闲时也会遵循这个存活时间规则,避免核心线程频繁销毁与创建。
- 任务队列(workQueue):选择合适的任务队列。对于串行任务,
LinkedBlockingQueue
是一个不错的选择。它是一个无界队列,可以缓存大量任务,当核心线程都在忙碌时,新的任务会被放入队列中等待执行,而不是立即创建新的非核心线程,从而减少线程的不必要创建。
- 设计任务执行逻辑
- 封装任务:将串行任务封装成一个大的任务单元,例如可以使用
CompletableFuture
来组合多个串行的子任务。假设我们有三个串行任务task1
、task2
、task3
,可以这样封装:
CompletableFuture<Void> combinedTask = CompletableFuture.runAsync(() -> task1())
.thenRun(() -> task2())
.thenRun(() -> task3());
- **使用`ExecutorService`提交任务**:通过`ExecutorService`将封装后的任务提交到线程池执行。
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(combinedTask);
Java线程池相关关键知识点
- 线程池的工作原理:线程池维护着一个线程队列和一个任务队列。当有任务提交时,首先会检查核心线程是否都在忙碌,如果有空闲核心线程,则分配任务给它执行;如果核心线程都在忙碌,则任务会被放入任务队列;当任务队列也满了,且当前线程数小于最大线程数时,会创建新的非核心线程来执行任务;如果线程数达到最大线程数且任务队列也满了,根据不同的拒绝策略处理新任务,例如直接抛出异常(
AbortPolicy
)、将任务回退给调用者(CallerRunsPolicy
)等。
- 线程池的类型:
- FixedThreadPool:固定大小的线程池,核心线程数和最大线程数相等,任务队列是
LinkedBlockingQueue
。适用于任务数量比较稳定,且执行时间较长的场景,避免了线程的频繁创建与销毁。
- CachedThreadPool:可缓存线程池,核心线程数为0,最大线程数为
Integer.MAX_VALUE
,任务队列是SynchronousQueue
。它适用于执行时间短且任务数量波动较大的场景,但在串行任务场景下可能会导致大量线程的频繁创建与销毁,不适合本题需求。
- SingleThreadExecutor:单线程的线程池,只有一个核心线程,任务队列是
LinkedBlockingQueue
。它保证所有任务按照顺序执行,类似串行任务,但只有一个线程,在处理多个串行任务时效率可能较低,不过能避免线程频繁创建销毁。
- ScheduledThreadPool:用于定时和周期性任务的线程池,在串行任务场景下,如果任务有定时执行需求,可以考虑使用。
- 拒绝策略:
- AbortPolicy:默认策略,当任务无法执行时抛出
RejectedExecutionException
。
- CallerRunsPolicy:将任务回退给调用者线程执行,这样可以减少新线程的创建,适用于任务提交速度过快的场景。
- DiscardPolicy:直接丢弃无法执行的任务,不抛出异常。
- DiscardOldestPolicy:丢弃任务队列中最老的任务,然后尝试提交新任务。