一、简介
什么是线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。虽然我们可以使用new的方式去创建线程,但若是并发线程太高,每个线程执行时间不长,这样频繁的创建销毁线程是比较耗费资源的,同时也不方便管理,线程池的出现就是为了解决上述问题。
线程池的优点
- 提高响应速度:通过线程池创建的线程,使用时直接通过线程池获取,不再需要手动创建线程,可以复用,及时响应。
- 降低资源的消耗:由于线程池被池化管理了,我们无需频繁地去创建销毁线程,那么资源消耗自然就降低了。
- 便于管理和监控:因为我们的工作线程都来自于线程池中,线程池可以对线程进行管理、监控、调优。
一个简单的线程池示例
public static void main(String[] args) {
//创建含有5个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//提交5个任务到线程池中
for (int i = 0; i < 5; i++) {
final int taskNo = i;
threadPool.execute(() -> {
System.out.println("执行任务:"+taskNo);
});
}
//关闭线程池
threadPool.shutdown();
//如果线程池还没达到Terminated状态,说明线程池中还有任务没有执行完
while (!threadPool.isTerminated()) {
}
}
输出结果:
执行任务:0
执行任务:4
执行任务:3
执行任务:2
执行任务:1
二、线程池的创建方式
线程池的创建方式可分为以下两类:
- 通过 Executors 创建线程池
- 通过 ThreadPoolExecutor 创建线程池
1、通过Excutors创建线程池
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口 ExecutorService。以下是一些重要的类和接口:
-
ExecutorService:真正的线程池接口,提供了执行、管理和控制任务的方法。
-
ScheduledExecutorService:继承自ExecutorService,能够解决那些需要任务重复执行的问题,支持定时和周期性任务执行。
-
ThreadPoolExecutor:ExecutorService的默认实现,提供了丰富的配置选项,如核心线程数、最大线程数、工作队列类型等。
-
ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor的ScheduledExecutorService接口实现,用于周期性任务调度。
Java中通过Executors工厂类提供了四种线程池,如下所示:
newFixedThreadPool: 创建一个固定大小的线程池,适用于控制最大并发数的场景,如果超出了那么就会在队列中等待。
newSingleThreadExecutor: 创建一个单线程化的线程池,只会有一个线程,用于保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行的场景。
newScheduledThreadPool: 创建一个定时线程池,用于定时及周期性任务执行的场景。
newCachedThreadPool: 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,否则新建线程;适用于执行大量短期异步任务的场景。
newFixedThreadPool
创建一个定长的线程池,适用于控制最大并发数的场景,如果超出了那么就会在队列中等待。源码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newSingleThreadExecutor
创建一个单线程化的线程池,只会有一个线程,用于保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行的场景。源码如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newScheduledThreadPool
创建一个定时线程池,用于定时及周期性任务执行的场景。源码如下:
//Executors类中
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
//ScheduledThreadPoolExecutor类中
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,否则新建线程;适用于执行大量短期异步任务的场景。源码如下:
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
观察上面的四种Excutor创建线程的方式,可以发现他们底层源码都是通过ThreadPoolExecutor去创建的线程池,下面将会介绍通过 ThreadPoolExecutor创建线程的方法及他的一些参数说明。
2、通过ThreadPoolExecutor创建线程池
为了解决Executors提供的四种快速创建线程池出现的OOM问题,推荐使用ThreadPoolExecutor的方式,按业务、按需创建线程池。通过设置合理的参数来灵活的使用线程池。
ThreadPoolExecutor核心参数:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
1. corePoolSize:核心线程数,即时空闲也会保留在线程池中的线程,他的生命周期是无限的,不会被回收。
2. maximumPoolSize:最大线程数,线程池允许创建的最大线程数,例如配置为10,那么线程池中最大的线程数就为10,若超过这个数那么就会触发线程拒绝策略。
3. keepAliveTime:核心线程数以外的线程的生存时间,例如corePoolSize为2,maximumPoolSize为5,假如我们线程池中有5个线程,核心线程以外有3个,这3个线程如果在keepAliveTime的时间内没有被用到就会被回收。
4. unit:keepAliveTime的时间单位。
5. workQueue:当核心线程都在忙碌时,任务都会先放到队列中。
6. threadFactory:线程工厂,用户可以通过这个参数指定创建线程的线程工厂。
7. handler:当线程池无法接受新的任务时,就会根据这个参数做出拒绝策略,默认拒绝策略是直接抛异常。
三、线程池队列
workQueque决定了缓存任务的排队策略,对于不同的业务场景,我们可以选择不同的工作队列。类型为BlockingQueue,查看他的源码可以看到线程池的队列可以有如下几种实现:
- ArrayBlockingQueue:由数组构成的有界阻塞队列,按照FIFO的方式对元素进行排序。
- LinkedBlockingQueue:由链表构成的队列,按照FIFO的对元素进行排序,任务默认的大小为Integer.MAX_VALUE,当然我们也可以设置链表容量大小。
- DelayQueue:延迟队列,提交的任务会按照执行时间进行从小到大的方式进行排序,否则就按照插入到队列的先后顺序进行排列。
- PriorityBlockingQueue:优先队列,按照优先级进行排序,是一个具备优先级的无界队列。
- SynchronousQueue:同步队列,是一个不能存储元素的阻塞队列,每一次向队列中插入数据必须等到另一个线程移除操作,否则插入操作会一直处于阻塞状态。
四、线程池的拒绝策略
线程池的拒绝策略用于在提交的任务队列已满时决定如何拒绝新的任务。以下是几种常见的线程池拒绝策略:
CallerRunsPolicy:当任务被拒绝添加时,如果调用者所在的线程不是线程池中的线程,那么任务将由该调用者所在的线程直接执行。
AbortPolicy:这是默认的拒绝策略,表示不能执行新的任务,并且直接抛出异常。
DiscardPolicy:这种拒绝策略,不能执行新的任务,但是也不抛出任何异常,只是简单地忽略。
DiscardOldestPolicy:如果执行程序没有空闲线程来执行新任务,则会丢弃队列中最老的任务,也就是最早进入队列的任务,然后尝试再次提交这个新的任务。
五、线程池的任务策略
1.如果当前线程池中的线程数量小于corePoolSize,则会创建一个线程执行此任务;
2.如果当前线程池中的线程数量大于corePoolSize,则会尝试将其添加到队列中,若添加成功,则该任务会排队等待线程将其取出进行执行;若队列中已达最大值,则添加失败,则会尝试创建新的线程执行这个任务;
3.如果当前线程池中的线程数量已经达到maximumPoolSize,则尝试创建新的线程结果为false,会采取任务拒绝策略;
4.如果线程池中线程数量大于corePoolSize,则当空闲时间超过keepAliveTime的线程,将会被终止,直到线程池数量不大于corePoolSize为止。
六、线程池的两种提交方式
线程池一共有两种提交方式,一个是execute,另一个是submit;对于execute来说,当任务提交到线程池中时直接按照流程执行即可,处理完成后是没有返回值的;而submit是用于有返回值的任务提交。
execute提交无返回值的线程例子:
public static void main(String[] args) {
//创建含有5个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//提交5个任务到线程池中
for (int i = 0; i < 5; i++) {
final int taskNo = i;
threadPool.execute(() -> {
System.out.println("执行任务:"+taskNo);
});
}
//关闭线程池
threadPool.shutdown();
//如果线程池还没达到Terminated状态,说明线程池中还有任务没有执行完
while (!threadPool.isTerminated()) {
}
}
submit提交有返回值的线程的例子:
@Test
public void test() throws ExecutionException, InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
final int taskNo = i;
Future<Integer> future = threadPool.submit(() -> {
System.out.println("执行任务:"+taskNo);
return taskNo;
});
System.out.println("处理结果:"+future.get());
}
//关闭线程池
threadPool.shutdown();
//如果线程池还没达到Terminated状态
while (!threadPool.isTerminated()) {
}
}
七、关闭线程池
线程池的停止方式有两种:
-
shutdown:本文中上述代码示例用的都是这种方式,使用这个方法之后,我们无法提交新的任务进来,线程池会继续工作,将手头的任务执行完再停止。
-
shutdownNow:强制停止,这种停止方式就比较粗暴了,线程池会直接将手头的任务都强行停止,且不接受新任务进来,线程停止立即生效。
注意:本文归作者所有,未经作者允许,不得转载