线程池
为什么需要线程池?
当我们想要程序并发执行的时候,通常会去手动创建一个新的线程,比如
new Thread(() -> {
并行任务代码...
}).start();
单个线程这么做确实没什么问题
但是,如果需要创建1000个甚至更多的线程呢?难道每次都需要new Thread吗?
创建线程需要消耗一些系统资源。首先,线程的创建需要开辟内存资源,包括本地方法栈、虚拟机栈和程序计数器等线程私有变量的内存。具体来说,每个线程都有其专属的内核数据,如OSID(线程的ID)和Context(存放CPU寄存器相关的资源)。这些上下文状态会被保存到Context中,以便下次使用。
其次,频繁的创建线程和销毁线程会带来一定的性能开销。但是,在同一进程中,只是创建第一个线程的时候需要申请资源,后续再创建新的线程都是共用同一份资源(节省了申请资源的开销);销毁线程的时候,只有销毁到最后一个线程的时候才真正释放资源,前面的线程销毁都不是真正的释放资源。
怎么解决呢?
那就是使用线程池,线程池通俗来讲就是一个"池子"里存放了固定存活的线程,只要你有任务了就可以去线程池中捞一个线程把任务交给它去处理,等它处理完任务了,再把线程放回池子里
此外,由于线程是反复利用的,所以降低了创建线程分配资源和销毁线程的开销,提高了响应速度。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
实践
Executors类提供4个静态工厂方法:newCachedThreadPool()、newFixedThreadPool(int)、newSingleThreadExecutor()和newScheduledThreadPool(int)。
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
// 提交任务给线程池
singleThreadExecutor.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程执行");
});
}
// 当线程池的任务都执行完了,关闭线程池
singleThreadExecutor.shutdown();
}
这里不一一写Executors提供的方法,其实就是改变了ExecutorService的实现方法
方法对比
工厂方法 | corePoolSize | maximumPoolSize | keepAliveTime | workQueue |
---|---|---|---|---|
newCachedThreadPool | 0 | Integer.MAX_VALUE | 60s | SynchronousQueue |
newFixedThreadPool | n(手动指定) | n(手动指定) | 0 | LinkedBlockingQueue |
newSingleThreadExecutor | 1 | 1 | 0 | LinkedBlockingQueue |
newScheduledThreadPool | corePoolSize | Integer.MAX_VALUE | 0 | DelayedWorkQueue |
自定义线程池
点进newSingleThreadExecutor方法可以看见,其实创建线程池是需要new一个ThreadPoolExecutor()对象,这个ThreadPoolExecutor对象决定了线程池的运行策略
参数说明
- corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,才会根据是否存在空闲线程,来决定是否需要创建新的线程。除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。
- maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
- keepAliveTime(线程存活保持时间):默认情况下,当线程池的线程个数多于corePoolSize时,如果线程在 keepAliveTime 的时间内 poll 不到任务,那我就认为这条线程没事做,可以干掉了。但只要keepAliveTime大于0,allowCoreThreadTimeOut(boolean) 方法也可将此超时策略应用于核心线程。另外,也可以使用setKeepAliveTime()动态地更改参数。
- unit(存活时间的单位):时间单位,分为7类,从细到粗顺序:NANOSECONDS(纳秒),MICROSECONDS(微妙),MILLISECONDS(毫秒),SECONDS(秒),MINUTES(分),HOURS(小时),DAYS(天);
- workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。可以使用此队列与线程池进行交互:
如果运行的线程数少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
如果运行的线程数等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。 - threadFactory(线程工厂):用于创建新线程。由同一个threadFactory创建的线程,属于同一个ThreadGroup,创建的线程优先级都为Thread.NORM_PRIORITY,以及是非守护进程状态。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号);
- handler(线程饱和策略):当线程池和队列都满了,则表明该线程池已达饱和状态。
ThreadPoolExecutor.AbortPolicy:处理程序遭到拒绝,则直接抛出运行时异常 RejectedExecutionException。(默认策略)
ThreadPoolExecutor.CallerRunsPolicy:调用者所在线程来运行该任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
ThreadPoolExecutor.DiscardPolicy:无法执行的任务将被删除。
ThreadPoolExecutor.DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重新尝试执行任务(如果再次失败,则重复此过程)。
线程池的执行过程
案例:
假设我们自定义一个线程池执行器
public ThreadPoolExecutor threadPoolExecutor() {
// 创建线程时会先执行threadFactory的newThread方法
ThreadFactory threadFactory的 = new ThreadFactory() {
private int count = 1;
@Override
public Thread newThread(@NotNull Runnable r) {
Thread thread = new Thread(r);
thread.setName("线程" + count);
count++;
return thread;
}
};
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), threadFactory);
return threadPoolExecutor;
}
参数:corePoolSize: 2, maximumPoolSize: 4, keepAliveTime: 100s, workQueue: ArrayBlockingQueue(容量为4)
提交2个任务到线程池
继续提交两个任务
此时线程池要处理的任务大于corePoolSize,就将未处理的任务放入任务队列中
继续往线程池提交4个任务
从上图可以看出两个特点:
- 线程池的线程增多了。这是因为当我们添加到第6个任务的时候,任务队列已经满了,放不下了;那这个时候maximumPoolSize参数就派上场了,maximumPoolSize决定了线程池最多能有多少个线程,一旦任务队列满了,再有新任务提交,就新增线程到线程池(找临时工干活);那临时工是怎么退掉的呢?临时线程如果在keepAliveTime时间内没事情干,就会被干掉(解雇)
- 新增的线程没有处理任务队列的第一个任务,而是去处理了队列满了之后的新增任务;所有我们看见了线程3去处理了任务7,线程4去处理了任务8
什么情况下会去执行handler?
当线程池和队列都满了,则表明该线程池已达饱和状态,这时候线程池就会去执行拒绝策略
有哪几种拒绝策略?
拒绝策略场景分析
(1)AbortPolicy
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), threadFactory, new ThreadPoolExecutor.AbortPolicy());
这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样能在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
(2)DiscardPolicy
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), threadFactory, new ThreadPoolExecutor.DiscardPolicy());
使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。
(3)DiscardOldestPolicy
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), threadFactory, new ThreadPoolExecutor.DiscardOldestPolicy());
此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
(4)CallerRunsPolicy
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
慎用!!!,假如提交任务的是主线程,那么可能会影响主线程任务的执行
为什么要自定义线程池
Fixed ThreadPool存在的问题就是它使用的是LinkedBlockingQueue,LinkedBlockingQueue的容量是没有上限的,一旦处理队列线程的速度跟不上线程入队的速度,很有可能产生OOM(内存溢出)
SingleThreadPool也有同样的问题
newCachedThreadPool存在的问题就是没有去限制最大的线程数
总结:
为什么不推荐使用Executors创建的线程池?很简单,虽然我们使用Executors创建了固定的线程数量,但是却没有指定任务队列的实现以及大小,一旦大量任务积压在任务队列中,可能会产生OOM(内存溢出)问题