1.线程池的作用
思考一个问题,我们为什么要引入线程池那么一个概念。首先,要知道我们引入线程就是为了解决频繁创建销毁进程带来的损耗,虽然创建/销毁线程的开销在变小了,但依旧不可忽视,如何进一步优化:
1.创建一个线程池,提前准备好线程,用完了也不要销毁,减少每次启动,销毁线程的损耗
2.引入轻量级线程->纤程/协程(Java21引入的虚拟线程),本质是程序猿在用户态代码中进行调度,不需要依靠内核的调度器,节省调度上的开销
2.线程池ThreadPoolExecutor的构造方法
-
corePoolSize(核心线程数):线程池中保持的最小线程数,即使线程是空闲的也会保留,除非设置了
allowCoreThreadTimeOut
。当有任务提交时,即使其他空闲的核心线程可用,也可能会创建新的线程,直到达到核心线程数。 -
maximumPoolSize(最大线程数):线程池中允许的最大线程数。当活动线程数达到这个值时,后续的任务将被阻塞,直到有线程可用为止。
-
keepAliveTime(线程空闲时间):当线程池中的线程数量大于核心线程数时,这是多余的空闲线程在终止之前等待新任务的最长时间。
-
TimeUnit(时间单位):用于指定 keepAliveTime 的时间单位,例如毫秒、秒、分钟等。
-
workQueue(工作队列):用于保存等待执行的任务的阻塞队列。常见的工作队列包括
ArrayBlockingQueue
、LinkedBlockingQueue
、SynchronousQueue
等。 -
threadFactory(线程工厂):用于创建新线程的工厂。通常情况下,可以通过自定义线程工厂来对线程进行定制,例如设置线程名字、优先级等.这里线程工厂也是一种设计模式,是由工厂类(对构造方法进一步封装后打包成的类,本质是因为构造方法达成方法重载的条件太苛刻)创建对象。
-
handler(拒绝策略):当工作队列已满且线程池中的线程数达到最大线程数时,新提交的任务将会被拒绝。拒绝策略定义了当任务被拒绝时的处理方式,常见的策略有
AbortPolicy
(默认)、CallerRunsPolicy
、DiscardPolicy
、DiscardOldestPolicy
等。AbortPolicy
:直接抛出RejectedExecutionException
异常。CallerRunsPolicy
:让提交任务的线程来执行任务。DiscardOldestPolicy
:丢弃队列中等待最久的任务。DiscardPolicy
:直接丢弃无法执行的任务。
3.Executor类的使用
由于ThreadPoolExecutor使用起来过于麻烦,我们也可以使用 Executors(对ThreadPoolExecutor进一步封装) 创建常见的线程池:
Executors
类提供了一系列静态工厂方法来创建不同类型的线程池。下面是一些常见的线程池创建方法:
-
newFixedThreadPool(int nThreads)
:创建一个固定大小的线程池,该线程池中的线程数量始终为指定的数量。 -
newCachedThreadPool()
:创建一个根据需要自动调整大小的线程池,该线程池中的线程数量会根据任务的数量动态调整。 -
newSingleThreadExecutor()
:创建一个单线程的线程池,该线程池中始终只有一个线程在工作,用于顺序执行任务。 -
newScheduledThreadPool(int corePoolSize)
:创建一个可以执行定时任务的线程池,可以指定核心线程数量,用于执行定时任务以及周期性任务。 -
newWorkStealingPool(int parallelism)
:创建一个使用工作窃取算法的线程池,用于处理大量并行任务,可以指定并行度。
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 创建固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// 创建单线程的线程池
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
// 创建带缓存的线程池,可以根据需要自动创建线程,线程数量不受限制
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 创建定时执行任务的线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
}
}
注意:ExecutorService
是一个接口,它是 Java 提供的用于管理线程池的高级工具之一。ThreadPoolExecutor
则是 ExecutorService
接口的一个实现类,它提供了灵活的线程池管理功能,可以通过设置各种参数来自定义线程池的行为
4.线程池的工作流程
线程池的工作流程通:
-
当有任务提交到线程池时,线程池会首先检查当前活动线程数是否小于核心线程数,如果是,则会创建新的线程来处理任务,否则进入下一步。
-
线程池会将任务添加到工作队列中等待执行。
-
如果工作队列已满且当前活动线程数未达到最大线程数,则会创建新的线程来处理任务。
-
如果工作队列已满且当前活动线程数已达到最大线程数,则根据指定的拒绝策略来处理新提交的任务。
5.线程数目的确定
需要综合考虑以下几个因素:
-
可用的处理器核心数:一般来说,线程数不应该超过处理器核心数,因为多余的线程会竞争 CPU 资源,导致上下文切换频繁,反而降低了性能。
-
任务的性质:如果任务是 CPU 密集型的(计算密集型),则线程数不宜过多(不超过核心数),因为大量的线程会增加上下文切换的开销,反而降低了性能。如果任务是 IO 密集型的(如网络请求、文件操作等),则可以适当增加线程数,以充分利用 IO 操作的等待时间。
-
任务的数量和大小:如果任务量很大或者任务的执行时间比较长,可以适当增加线程数,以提高任务的响应速度。但是需要注意避免过度创建线程,导致系统资源耗尽或性能下降。
-
系统资源:线程数也受到系统资源的限制,包括内存、文件描述符等。如果创建过多的线程会导致系统资源不足,可能会影响系统的稳定性和性能。
-
特定应用场景的需求:有些特定的应用场景可能需要特定数量的线程,例如高并发的网络服务器、数据处理系统等。
总之,可以根据具体情况来确定线程池中线程的数量。通常建议先根据系统的配置和需求初步设置线程数,然后通过性能测试和监控来动态调整线程数,以达到最佳的性能和稳定性。