线程池是帮助我们管理线程的工具,它维护了多个线程,可以降低资源的消耗,提高系统的性能。
并且通过使用线程池,我们开发人员可以更好的把精力放在任务代码上,而不去管线程是如何执行的,实现任务提交和执行的解藕。
本文将从是何、为何、如何的角度来讲解线程池:
线程池是一种池化的技术,类似的还有数据库连接池、HTTP 连接池等等。
池化的思想主要是为了减少每次获取和结束资源的消耗,提高对资源的利用率。
比如在一些偏远地区打水不方便的,大家会每段时间把水打过来存在池子里,这样平时用的时候就直接来取就好了。
线程池同理,正是因为每次创建、销毁线程需要占用太多系统资源,所以我们建这么一个池子来统一管理线程。用的时候从池子里拿,不用了就放回来,也不用你销毁,是不是方便了很多?
Java 中的线程池是由 juc
即 java.util.concurrent
包来实现的,最主要的就是 ThreadPoolExecutor
这个类。具体怎么用我们下文再说。
在多线程的第一篇文章中我们说过,进程会申请资源,拿来给线程用,所以线程是很占用系统资源的,那么我们用线程池来统一管理线程就能够很好的解决这种资源管理问题。
比如因为不需要创建、销毁线程,每次需要用的时候我就去拿,用完了之后再放回去,所以节省了很多资源开销,可以提高系统的运行速度。
而统一的管理和调度,可以合理分配内部资源,根据系统的当前情况调整线程的数量。
那总结来说有以下 3 个好处:
说了这么多,终于到了今天的重点,我们来看下究竟怎么用线程池吧~
Java 给我们提供了 Executor
接口来使用线程池。
Executor
我们常用的线程池有这两大类:
它俩的区别呢,就是第一个是普通的,第二个是可以定时执行的。
当然还有其他线程池,比如 JDK 1.7 才出现的 ForkJoinPool
,可以把大任务分割成小任务来执行,最后再大一统。
那么任务提交到一个线程池之后,它会经历一个怎样的过程呢?
线程池在内部实际上采用了生产者消费者模型(还不清楚这个模型的在文章开头有改文章的链接)将线程和任务解藕,从而使线程池同时管理任务和线程。
当任务提交到线程池里之后,需要经过以下流程:
执行过程
BlockingQueue
,在生产者消费者这节里提到过。我们主要说下 ThreadPoolExecutor
,它是最常用的线程池。
ThreadPoolExecutor Structure
这里我们可以看到,这个类里有 4 个构造方法,点进去仔细看,其实前三个都 call 了最后一个,所以我们只需要看最后一个就好。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
这里我们来仔细看下这几个参数:
corePoolSize the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set
maximumPoolSize the maximum number of threads to allow in the pool
keepAliveTime when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
unit the time unit for the {@code keepAliveTime} argument
workQueue the queue to use for holding tasks before they are executed.
threadFactory the factory to use when the executor creates a new thread
handler the handler to use when execution is blocked because the thread bounds and queue capacities are reached
所以我们可以通过自己传入这 7 个参数构造线程池,当然了,贴心的 Java 也给我们包装好了几类线程池可以很方便的拿来使用。
我们具体来看每个的含义和用法。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这里我们可以看到,
Integer.MAX_VALUE
;它的适用场景在源码里有说:
These pools will typically improve the performance of programs that execute many short-lived asynchronous tasks.
来看怎么用:
public class newCacheThreadPool {
public static void main(String[] args) {
// 创建一个线程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 向线程池提交任务
for (int i = 0; i < 50; i++) {
executorService.execute(new Task());//线程池执行任务
}
executorService.shutdown();
}
}
执行结果:
newCached 结果
可以很清楚的看到,线程 1、2、3、5、6 都很快重用了。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
这个线程池的特点是:
它的适用场景是:
Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue.
public class FixedThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 200; i++) {
executorService.execute(new Task());
}
executorService.shutdown();
}
}
newFixed 结果
这里我限制了线程池里最多有 10 个线程,哪怕有 200 个任务需要执行,也只有 1-10 这 10 个线程可以运行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
这个线程池顾名思义,里面只有 1 个线程。
适用场景是:
Creates an Executor that uses a single worker thread operating off an unbounded queue.
我们来看下效果。
public class SingleThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100; i++) {
executorService.execute(new Task());
}
executorService.shutdown();
}
}
newSingle 结果
这里在出结果的时候我能够明显的感觉到有些卡顿,这在前两个例子里是没有的,毕竟这里只有一个线程在运行嘛。
所以在使用线程池时,其实都是调用的 ThreadPoolExecutor
这个类,只不过传递的不同参数。
这里要特别注意两个参数:
workQueue
的选择,这个就是阻塞队列的选择,如果要说还得这么一大篇文章,之后有机会再写吧。handler
的设置。那我们发现,在上面的 3 个具体线程池里,其实都没有设定 handler
,这是因为它们都使用了 defaultHandler
。
/**
* The default rejected execution handler
*/
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
在 ThreadPoolExecutor
里有 4 种拒绝策略,这 4 种策略都是 implements
了 RejectedExecutionHandler
:
AbortPolicy
表示拒绝任务并抛出一个异常 RejectedExecutionException
。这个我称之为“正式拒绝”,比如你面完了最后一轮面试,最终接到 HR 的拒信。DiscardPolicy
拒绝任务但不吭声。这个就是“默拒”,比如大部分公司拒简历的时候都是默拒。DiscardOldestPolicy
顾名思义,就是把老的任务丢掉,执行新任务。CallerRunsPolicy
直接调用线程处理该任务,就是 VIP 嘛。所以这 3 种线程池都使用的默认策略也就是第一种,光明正大的拒绝。
好了以上就是本文的所有内容了。当然线程池还有很多知识点,比如 execute()
和 submit()
方法,线程池的生命周期等等。