线程池
1159字约4分钟
2024-08-08
Java
语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间
如果可以复用一组线程,那么我们就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
Java
标准库提供了 ExecutorService
接口表示线程池,它的典型用法如下
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
// 关闭线程池
executor.shutdownNow();
ExecutorService
只是接口,Java
标准库提供的几个常用实现类有
FixedThreadPool
:线程数固定的线程池CachedThreadPool
:线程数根据任务动态调整的线程池SingleThreadExecutor
:仅单线程执行的线程池ScheduledThreadPool
:适用于执行延时或者周期性任务
创建这些线程池的方法都被封装到 Executors
这个类中。我们以 FixedThreadPool
为例,看看线程池的执行逻辑
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 创建一个固定大小的线程池:
ExecutorService es = Executors.newFixedThreadPool(4);
for (int i = 0; i < 6; i++) {
es.submit(new Task("" + i));
}
// 关闭线程池:
es.shutdown();
}
}
class Task implements Runnable {
private final String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("start task " + name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end task " + name);
}
}
我们观察执行结果,一次性放入 6
个任务,由于线程池只有固定的 4
个线程,因此,前 4
个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务
线程池在程序结束的时候要关闭。使用 shutdown()
方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow()
会立刻停止正在执行的任务,awaitTermination()
则会等待指定的时间让线程池关闭
如果我们把线程池改为 CachedThreadPool
,由于这个线程池的实现会根据任务数量动态调整线程池的大小,所以 6
个任务可一次性全部同时执行
如果我们想把线程池的大小限制在 4~10
个之间动态调整怎么办?我们查看 Executors.newCachedThreadPool()
方法的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
因此,想创建指定动态范围的线程池,可以这么写
int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,
60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
ScheduledThreadPool
还有一种任务,需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用 ScheduledThreadPool
。放入 ScheduledThreadPool
的任务可以定期反复执行
创建一个 ScheduledThreadPool
仍然是通过 Executors
类
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
我们可以提交一次性任务,它会在指定延迟后只执行一次
// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
如果任务以固定的每 3
秒执行,我们可以这样写
// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
如果任务以固定的 3
秒为间隔执行,我们可以这样写
// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
注意FixedRate和FixedDelay的区别
FixedRate
是指任务总是以固定时间间隔触发,不管任务执行多长时间FixedDelay
是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务
在FixedRate模式下,假设每秒触发,如果某次任务执行时间超过1秒,后续任务会不会并发执行?
如果此任务的任何执行时间超过其周期,则后续执行可能会延迟开始,但不会并发执行。
如果任务抛出了异常,后续任务是否继续执行?
如果任务的任何执行遇到异常,则将禁止后续任务的执行。