一、概述
1.什么是线程池
ThreadPool是缓存了一定数量线程的一块区域,ThreadPool中的线程不能手动开始,也不能手动取消,只要把工作函数排入线程池,剩下的工作将由系统自动完成,也就是说我们不能控制线程池中的线程。如果想对想对线程更多的控制,那就不适合使用线程池了。
2.什么情况适合下使用
为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率, ThreadPool适于并发运行若干个运行时间不长且互不干扰的函数。
3.作用&优势
(1)线程复用
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
(2)线程管理
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
4.什么情况下不适合使用&劣势
- 线程执行需要很长时间
- 需要为线程指定详细的优先级
- 在执行过程中需要对线程进行操作,比如睡眠、挂起等
二、线程池的使用
1.线程池的创建
(1)ThreadPoolExecutor是线程池的真正实现类,通过ThreadPoolExecutor来创建一个线程池
// 通过构造方法&配置核心参数,创建线程池对象
Executor threadPool = new ThreadPoolExecutor( CORE_POOL_SIZE , MAXIMUM_POOL_SIZE ,
KEEP_ALIVE , TimeUnit.SECONDS,
sPoolWorkQueue , sThreadFactory );
//构造函数源码分析
new ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //线程池最大线程数
long keepAliveTime, //线程活动保持时间
TimeUnit unit, //线程活动保持时间单位
BlockingQueue<Runnable> workQueue, //任务队列
ThreadFactory threadFactory); //线程工厂
(2)线程池中有6个核心参数
- corePoolSize:核心线程数。默认情况下,核心线程会一直存活(包括空闲状态)。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程,等到需要执行的任务数大于corePoolSize(线程池核心线程数)时就不会再创建了。
- maximumPoolSize:线程池最大线程数。如果队列满了,并且已创建的线程数小于maximumPoolSize(线程池最大线程数),则线程池会再创建新的线程执行任务;当活动线程数达到maximumPoolSize(线程池最大线程数)后,后续的新任务将会阻塞。
- keepAliveTime:线程活动保持时间。默认情况下,该时间为非核心线程闲置后,保存存活的时间。当allowCoreThreadTimeout设置为true时,该时间同样作用于核心线程。当超过该时间,空闲的工作线程就会被回收。(如果任务很多,并且每个任务执行的时间比较短,可以调大该时间,提高线程的利用率)。
- unit:keepAliveTime参数的时间单位(线程活动保持时间单位)。常用单位有小时(HOURS)、分(MINUTES)、秒(SECONDS)、毫秒(MILLISECONDS)。
- workQueue:任务队列。属于阻塞队列BlockQueue类型,即当队列为空时,此时取出任务的操作会被阻塞;当队列满时,添加任务也会被被阻塞。通过线程池的execute()方法提交的Runnable对象,将存储在该参数中。
- theadFactory:线程工厂。为线程池创建新线程。我们一般使用默认即可。
2.向线程池提交任务
//向线程池提交任务:execute()
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 线程执行任务
}
});
可以使用execute提交任务,但是execute方法没有返回值,所以无法判断任务是否被线程池执行成功。通过上边的代码可知execute()方法输入的任务是一个Runnable类的实例。
也可以使用submit()方法来提交任务,它会返回一个future,那么可以通过future来判断任务是否执行成功,future的get方法获取返回值,get方法会阻塞直到任务完成。
Future<Object> future = threadPool.submit(harReturnValuetask);
try {
Object o = future.get();
} catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
} finally {
// 关闭线程池
threadPool.shutdown();
}
3.线程池的关闭
//关闭线程池shutdown()
threadPool.shutdown();
//或者threadPool.shutdownNow();
(1)原理
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
(2)区别 shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表; shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
(3)选择 只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown()来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow()来关闭线程池。
三、合理的配置线程池
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
- 任务的优先级:高、中和低。
- 任务的执行时间:长、中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
- 性质不同的任务:可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池;IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu;混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。比如:
corePoolSize = Runtime.getRuntime().availableProcessors() + 1
maximumPoolSize = Runtime.getRuntime().availableProcessors() * 2 + 1
-
优先级不同的任务:可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
-
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
-
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
四、分析线程池的工作原理
1.工作流程
线程池处理任务的优先级:核心线程数(corePoolSize) > 任务队列(workQueue) > 最大线程数(maximumPoolSize):
a. 首先线程池判断 核心线程(corePoolSize) 是否已满?没满,创建一个新线程来执行任务。满了,则进入下个流程。
b. 其次线程池判断 任务队列(workQueue) 是否已满?没满,则将新提交的任务存储在任务队列里。满了,则进入下个流程。
c. 最后线程池判断 最大线程数(maximumPoolSize) 是否已满?没满,则创建一个新线程来执行任务。满了,表示该任务无法执行,则交给饱和策略来处理这个任务。
handler: 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。
2.特别注意
当线程池中的线程数量 > 核心线程数(corePoolSize)时,若某线程(非核心线程)的空闲时间 > 闲置超时时长(keepAliveTime) 线程将被终止。通过这样的策略,线程池可动态调整池中的线程数。
五、常见线程池分类
根据参数的不同配置,Java中最常见的线程池有4类。即对于下面4类线程池,Java已根据应用场景配置好核心参数。具体介绍如下:
- 定长线程池(FixedThreadPool)
- 定时线程池(ScheduledThreadPool )
- 可缓存线程池(CachedThreadPool)
- 单线程化线程池(SingleThreadExecutor)
1.定长线程池(FixedThreadPool)
特点:只有核心线程 & 不会被回收、线程数量固定、任务队列无大小限制(超出的线程任务会在队列中等待)
应用场景:控制线程最大并发数
具体使用:通过Executors.newFixedThreadPool()创建
示例:
// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run(){
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务:execute()
fixedThreadPool.execute(task);
// 4. 关闭线程池
fixedThreadPool.shutdown();
2.定时线程池(ScheduledThreadPool )
特点:核心线程数量固定、非核心线程数量无限制(闲置时马上回收)
应用场景:执行定时 / 周期性 任务
使用:通过Executors.newScheduledThreadPool()创建
示例:
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run(){
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务:schedule()
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
// 4. 关闭线程池
scheduledThreadPool.shutdown();
3.可缓存线程池(CachedThreadPool)
特点:只有非核心线程、线程数量不固定(可无限大)、灵活回收空闲线程(具备超时机制,全部回收时几乎不占系统资源)、新建线程(无线程可用时);任何线程任务到来都会立刻执行,不需要等待
应用场景:执行大量、耗时少的线程任务
使用:通过Executors.newCachedThreadPool()创建
示例:
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run(){
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务:execute()
cachedThreadPool.execute(task);
// 4. 关闭线程池
cachedThreadPool.shutdown();
//当执行第二个任务时第一个任务已经完成
//那么会复用执行第一个任务的线程,而不用每次新建线程。
4.单线程化线程池(SingleThreadExecutor)
特点:只有一个核心线程(保证所有任务按照指定顺序在一个线程中执行,不需要处理线程同步的问题)
应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作,文件操作等
使用:通过Executors.newSingleThreadExecutor()创建
示例:
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run(){
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务:execute()
singleThreadExecutor.execute(task);
// 4. 关闭线程池
singleThreadExecutor.shutdown();
六、线程池的监控
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用。
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
- largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
- getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
- getActiveCount:获取活动的线程数。
通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:
protected void beforeExecute(Thread t, Runnable r) { }