java线程池使用指南
目录
线程池简介
java线程池是一种用于管理和复用线程的机制。它包含一组预先创建的线程,这些线程可以被重复使用来执行任务,而不需要为每个任务都创建和销毁线程。
线程池的主要目的是提高多线程应用程序的性能和效率,同时减少线程创建和销毁的开销。通过合理配置线程池的大小和参数,可以更好地利用系统资源,避免过多的线程竞争和线程创建/销毁的开销。
线程池通常由以下几个核心组件组成:
- 线程池管理器(ThreadPoolExecutor):负责创建和管理线程池,包括线程创建、销毁、任务调度等操作。
- 工作线程(Worker Threads):线程池中的线程,用于执行提交给线程池的任务。
- 任务队列(Task Queue):用于存储待执行的任务。当线程池中的线程空闲时,它们会从任务队列中获取任务并执行。
- 任务(Task):要执行的工作单元。可以是实现了 Runnable 接口或 Callable 接口的任务对象。
ThreadPoolExecutor运行原理
线程池的好处
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行
创建线程池
线程池构造函数的参数
参数名 | 类型 | 含义 |
---|---|---|
corepoolsize | int | 核心线程数 |
maxpoolsize | int | 最大线程数 |
keepalivetime | long | 保持存活时间 |
workQueue | blockingQueue | 任务存储队列 |
threadFactory | threadFactory | 当线程池需要新的线程时,会使用threadFactory来生成新的线程 |
handler | RejectedExecutionHandler | 由于线程池无法接受你提交的任务的拒绝策略 |
参数详细解释 |
- corePoolSize指的是核心线程数:线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时, 再创建新线程去执行任务 线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限,这就是最大量maxPoolSize
- 存活时间:线程数超过codesize后,多余的线程会存活到设置定的时间后,自动销毁
- threadFactory:新线程创建的工厂方法,
- handler:拒绝执行策略,默认为AbortPolicy
线程池队列
线程池原理
- 如果线程数小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行新任务。
- 如果线程数等于(或大于)corePoolSize但少于 maximumPoolSize,则将任务放入队列。
- 如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程来运行任务。
- 如果队列已满,并且线程数大于或等于maxPoolSize, 则拒绝该任务。
线程池状态
运行状态 | 状态描述 |
---|---|
RUNNING | 能接受新提交的任务,并且也能处理阻塞队列中的任务。 |
SHUTDOWN | 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。 |
STOP | 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。 |
TIDYING | 所有的任务都已终止了,workerCount(有效线程数)为0。 |
TERMINATED | 在terminated0 方法执行完后进入该状态。 |
提交任务
任务提交的方法有如下:
- execute(Runnable command): 提交一个任务给线程池进行执行。根据线程池的状态和工作线程数量,决定是创建新线程来执行任务还是排队任务或者拒绝任务。
- Future> submit(Runnable task) 提交一个任务给线程池,并返回一个Future,由于runable 无返回值,不能获取到执行任务的返回结果,但是可以通过future获取当前任务的状态
- Future submit(Callable task) 这个方法的参数是一个 Callable 接口,它只有一个 call() 方法,并且这个方法是有返回值的,所以这个方法返回的 Future 对象可以通过调用其 get() 方法来获取任务的执行结果。
- Future submit(Runnable task, T result) 假设这个方法返回的 Future 对象是 f,f.get() 的返回值就是传给 submit() 方法的参数 result。这个方法该怎么用呢?下面这段示例代码展示了它的经典用法。需要你注意的是 Runnable 接口的实现类 Task 声明了一个有参构造函数 Task(Result r) ,创建 Task 对象的时候传入了 result 对象,这样就能在类 Task 的 run() 方法中对 result 进行各种操作了。result 相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据
线程停止方式
线程停止的方法如下:
- shutdown() 等待所有任务执行完后,再关闭所有线程。建议用这种方式停止线程
- shutdownnow() 立即停止所有线程,不管提交任务执行到哪里
线程数数量设置
线程数的设置要根据不同场景来合理设置,本文提供一个基于经验来设置线程数的方法。实际要根据应用进行压测后,给出一个实际值。
- cpu密集型 线程数= cpu核心数+1
- io密集型 线程数= cpu核心数*(1+平均等待时间/平均工作时间)
以上设置都是经验值,实际要根据业务场景压测出来一个合理的值。
线程池最佳实践
使用线程池时,以下是一些最佳实践,可以帮助您获得最佳性能和可靠性:
- 选择合适的线程池大小:根据应用程序的需求和系统资源,选择合适的核心线程数和最大线程数。不要盲目增加线程数,过多的线程可能会导致资源竞争和性能下降。
- 使用合适的阻塞队列:选择适合您应用程序需求的阻塞队列类型。常见的阻塞队列包括无界队列(如 LinkedBlockingQueue)和有界队列(如 ArrayBlockingQueue)。无界队列可能会导致内存溢出,因此需要谨慎使用。
- 设置适当的线程池参数:根据应用程序的特性和负载情况,设置合适的参数,例如空闲线程的存活时间(keepAliveTime)、是否允许核心线程超时(allowCoreThreadTimeOut)等。
- 使用合适的拒绝策略:当线程池无法接受新的任务时,通过设置合适的拒绝策略来处理。常见的拒绝策略包括丢弃策略(DiscardPolicy)、丢弃最旧的任务策略(DiscardOldestPolicy)、抛出异常策略(AbortPolicy)和调用者运行策略(CallerRunsPolicy)。
- 考虑任务的优先级:如果您的应用程序中有不同优先级的任务,可以使用带有优先级的队列(如 PriorityBlockingQueue)来确保高优先级的任务能够尽快执行。
- 处理异常:正确处理任务中可能发生的异常,避免线程池因为未捕获的异常而终止。可以通过在任务中捕获异常并记录日志,或者使用适当的 UncaughtExceptionHandler 来处理异常。
- 合理设计任务:确保任务的执行时间不会太长,避免长时间阻塞线程池中的线程。如果可能,将长时间运行的任务拆分为多个较小的任务,以便更好地利用线程池中的线程。
- 及时关闭线程池:当不再需要线程池时,应该及时关闭它,以释放资源并避免可能的内存泄漏。可以使用 shutdown() 方法来优雅地关闭线程池,或者使用 shutdownNow() 方法立即关闭线程池。
- 监控和调优:监控线程池的性能指标,例如活动线程数、任务完成数、任务队列大小等,以便及时发现性能瓶颈和问题,并根据需要进行调优。