前面介绍了线程的基本用法,以及多线程并发的问题处理,但实际开发中每每存在许多性质类似的任务,好比批量发送消息、批量下载文件、批量进行交易等等。这些同类任务的处理流程一致,不存在资源共享问题,相互之间也不须要通讯交互,总之每一个任务均可以看做是单独的事务,仿佛流水线上的原材料通过一系列步骤加工以后变为成品。可要是开启分线程的话,得对每项任务都分别建立新线程并予以启动,且不说如何的费时费力,单说这批量操做有多少任务就要开启多少分线程,系统的有限资源禁不起这么多的线程同时过来折腾。
就像工厂里的流水线,每条流水线的生产速度是有限的,一会儿涌来大量原材料,一条流水线也消化不了,得多开几条流水线才行。可是流水线也不能想开就开,毕竟每开一条流水线都要占用工厂地盘,并且流水线开多了的话,后续没有这么多原材料的时候,岂不是形成资源浪费?到时又得关闭多余的流水线,纯属人傻钱多瞎折腾。因此呢,合理的作法应当是先开少数几条流水线,假若有大批来料须要加工,再多开几条流水线,并且这些流水线要进行统一调度管理,新加的原料得放到空闲的流水线上加工,而不是再开新的流水线,这样才能在最大程度上节约生产资源、提升工做效率。
Java体系之中,若将线程比做流水线的话,好几个常驻的运行线程便组成了批量处理的工厂,那么工厂里面统一管理这些流水线的调度中心则被称为“线程池”。线程池封装了线程的建立、启动、关闭等操做,以及系统的资源分配与线程调度;它还支持任务的添加和移除功能,使得程序员能够专心编写任务代码的业务逻辑,没必要操心线程怎么跑这些细枝末节。Java提供的线程池工具最经常使用的是ExecutorService及其派生类ThreadPoolExecutor,它支持如下四种线程池类型:
一、只有一个线程的线程池,该线程池由Executors类的newSingleThreadExecutor方法建立而来。它的建立代码示例以下:html
// 建立一个只有一个线程的线程池 ExecutorService pool = (ExecutorService) Executors.newSingleThreadExecutor();
二、拥有固定数量线程的线程池,该线程池由Executors类的newFixedThreadPool方法建立而来,方法参数即为线程数量。它的建立代码示例以下:程序员
// 建立一个线程数量为3的线程池 ExecutorService pool = (ExecutorService) Executors.newFixedThreadPool(3);
三、拥有无限数量线程的线程池,该线程池由Executors类的newCachedThreadPool方法建立而来。它的建立代码示例以下:多线程
// 建立一个不限制线程数量的线程池 ExecutorService pool = (ExecutorService) Executors.newCachedThreadPool();
四、线程数量容许变化的线程池,该线程池须要调用ThreadPoolExecutor的构造方法来建立,构造方法的输入参数按顺序说明以下:
第一个参数是个整型数,名叫corePoolSize,它指定了线程池的最小线程个数。
第二个参数也是个整型数,名叫maximumPoolSize,它指定了线程池的最大线程个数。
第三个参数是个长整数,名叫keepAliveTime,它指定了每一个线程保持活跃的时长,若是某个线程的空闲时间超过这个时长,则该线程会结束运行,直到线程池中的线程总数等于corePoolSize为止。
第四个参数为TimeUnit类型,名叫unit,它指定了第三个参数的时间单位,好比TimeUnit.SECONDS表示时间单位是秒。
第五个参数为BlockingQueue类型,它指定了待执行线程所处的等待队列。
第四种线程池(自定义线程池)的建立代码示例以下:并发
// 建立一个自定义规格的线程池(最小线程个数为2,最大线程个数为5,每一个线程保持活跃的时长为60,时长单位秒,等待队列大小为19) ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(19));
建立好了线程池以后,便可调用线程池对象的execute方法将指定任务加入线程池。须要注意的是,execute方法并不必定马上执行指定任务,只有当线程池中存在空闲线程或者容许建立新线程之时,才会立刻执行任务;不然会将该任务放到等待队列,而后按照排队顺序在方便的时候再一个一个执行队列中的任务。除了execute方法方法,ExecutorService还提供了若干查询与调度方法,这些方法的用途简介以下:
getCorePoolSize:获取核心的线程个数(即线程池的最小线程个数)。
getMaximumPoolSize:获取最大的线程个数(即线程池的最大线程个数)。
getPoolSize:获取线程池的当前大小(即线程池的当前线程个数)。
getTaskCount:获取全部的任务个数。
getActiveCount:获取活跃的线程个数。
getCompletedTaskCount:获取已完成的任务个数。
remove:从等待队列中移除指定任务。
shutdown:关闭线程池。关闭以后不能再往线程池中添加任务,不过要等已添加的任务执行完,才最终关掉线程池。
shutdownNow:当即关闭线程池。以后一样不能再往线程池中添加任务,同时会给已添加的任务发送中断信号,直到全部任务都退出才最终关掉线程池。
isShutdown:判断线程池是否已经关闭。ide
接下来作个实验,看看几种线程池是否符合预期的运行方式。实验开始前先定义一个操做任务,很简单,仅仅打印本次的操做日志,包括操做时间、操做线程、操做描述等信息。操做任务的代码例子以下所示:工具
// 定义一个操做任务 private static class Operation implements Runnable { private String name; // 任务名称 private int index; // 任务序号 public Operation(String name, int index) { this.name = name; this.index = index; } @Override public void run() { // 如下打印操做日志,包括操做时间、操做线程、操做描述等信息 String desc = String.format("%s执行到了第%d个任务", name, index+1); PrintUtils.print(Thread.currentThread().getName(), desc); } };
而后分别命令每种线程池各自启动十个上述的操做任务。首先是单线程的线程池,它的实验代码示例以下:测试
// 测试单线程的线程池 private static void testSinglePool() { // 建立一个只有一个线程的线程池 ExecutorService pool = (ExecutorService) Executors.newSingleThreadExecutor(); for (int i=0; i<10; i++) { // 循环启动10个任务 // 建立一个操做任务 Operation operation = new Operation("单线程的线程池", i); pool.execute(operation); // 命令线程池执行该任务 } pool.shutdown(); // 关闭线程池 }
运行以上的实验代码,观察到以下的线程池日志:this
22:22:43.959 pool-1-thread-1 单线程的线程池执行到了第1个任务 22:22:43.960 pool-1-thread-1 单线程的线程池执行到了第2个任务 22:22:43.961 pool-1-thread-1 单线程的线程池执行到了第3个任务 22:22:43.961 pool-1-thread-1 单线程的线程池执行到了第4个任务 22:22:43.962 pool-1-thread-1 单线程的线程池执行到了第5个任务 22:22:43.962 pool-1-thread-1 单线程的线程池执行到了第6个任务 22:22:43.962 pool-1-thread-1 单线程的线程池执行到了第7个任务 22:22:43.963 pool-1-thread-1 单线程的线程池执行到了第8个任务 22:22:43.963 pool-1-thread-1 单线程的线程池执行到了第9个任务 22:22:43.963 pool-1-thread-1 单线程的线程池执行到了第10个任务
由日志可见,单线程的线程池始终只有一个名叫pool-1-thread-1的线程在执行任务。线程
继续测试固定数量的线程池,它的实验代码示例以下:日志
// 测试固定数量的线程池 private static void testFixedPool() { // 建立一个线程数量为3的线程池 ExecutorService pool = (ExecutorService) Executors.newFixedThreadPool(3); for (int i=0; i<10; i++) { // 循环启动10个任务 // 建立一个操做任务 Operation operation = new Operation("固定数量的线程池", i); pool.execute(operation); // 命令线程池执行该任务 } pool.shutdown(); // 关闭线程池 }
运行以上的实验代码,观察到以下的线程池日志:
22:23:15.141 pool-1-thread-1 固定数量的线程池执行到了第1个任务 22:23:15.141 pool-1-thread-2 固定数量的线程池执行到了第2个任务 22:23:15.141 pool-1-thread-3 固定数量的线程池执行到了第3个任务 22:23:15.142 pool-1-thread-1 固定数量的线程池执行到了第4个任务 22:23:15.142 pool-1-thread-3 固定数量的线程池执行到了第5个任务 22:23:15.142 pool-1-thread-2 固定数量的线程池执行到了第6个任务 22:23:15.142 pool-1-thread-3 固定数量的线程池执行到了第7个任务 22:23:15.143 pool-1-thread-2 固定数量的线程池执行到了第8个任务 22:23:15.143 pool-1-thread-1 固定数量的线程池执行到了第9个任务 22:23:15.143 pool-1-thread-2 固定数量的线程池执行到了第10个任务
由日志可见,固定数量的线程池一共开启了三个线程去执行任务。
再来测试无限数量的线程池,它的实验代码示例以下:
// 测试无限数量的线程池 private static void testUnlimitPool() { // 建立一个不限制线程数量的线程池 ExecutorService pool = (ExecutorService) Executors.newCachedThreadPool(); for (int i=0; i<10; i++) { // 循环启动10个任务 // 建立一个操做任务 Operation operation = new Operation("无限数量的线程池", i); pool.execute(operation); // 命令线程池执行该任务 } pool.shutdown(); // 关闭线程池 }
运行以上的实验代码,观察到以下的线程池日志:
22:25:52.344 pool-1-thread-6 无限数量的线程池执行到了第6个任务 22:25:52.344 pool-1-thread-3 无限数量的线程池执行到了第3个任务 22:25:52.344 pool-1-thread-5 无限数量的线程池执行到了第5个任务 22:25:52.344 pool-1-thread-8 无限数量的线程池执行到了第8个任务 22:25:52.344 pool-1-thread-7 无限数量的线程池执行到了第7个任务 22:25:52.344 pool-1-thread-4 无限数量的线程池执行到了第4个任务 22:25:52.344 pool-1-thread-1 无限数量的线程池执行到了第1个任务 22:25:52.344 pool-1-thread-9 无限数量的线程池执行到了第9个任务 22:25:52.344 pool-1-thread-2 无限数量的线程池执行到了第2个任务 22:25:52.344 pool-1-thread-10 无限数量的线程池执行到了第10个任务
由日志可见,无限数量的线程池真的没限制线程个数,有多少任务就启动多少线程,虽然跑得很快可是系统压力也大。
最后是自定义的线程池,它的实验代码示例以下:
// 测试自定义的线程池 private static void testCustomPool() { // 建立一个自定义规格的线程池(最小线程个数为2,最大线程个数为5,每一个线程保持活跃的时长为60,时长单位秒,等待队列大小为19) ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(19)); for (int i=0; i<10; i++) { // 循环启动10个任务 // 建立一个操做任务 Operation operation = new Operation("自定义的线程池", i); pool.execute(operation); // 命令线程池执行该任务 } pool.shutdown(); // 关闭线程池 }
运行以上的实验代码,观察到以下的线程池日志:
22:28:46.337 pool-1-thread-1 自定义的线程池执行到了第1个任务 22:28:46.337 pool-1-thread-2 自定义的线程池执行到了第2个任务 22:28:46.338 pool-1-thread-2 自定义的线程池执行到了第4个任务 22:28:46.338 pool-1-thread-1 自定义的线程池执行到了第3个任务 22:28:46.339 pool-1-thread-2 自定义的线程池执行到了第5个任务 22:28:46.339 pool-1-thread-1 自定义的线程池执行到了第6个任务 22:28:46.339 pool-1-thread-2 自定义的线程池执行到了第7个任务 22:28:46.339 pool-1-thread-1 自定义的线程池执行到了第8个任务 22:28:46.340 pool-1-thread-2 自定义的线程池执行到了第9个任务 22:28:46.340 pool-1-thread-1 自定义的线程池执行到了第10个任务
由日志可见,自定义的线程池一般仅保持最小量的线程数,只有短期涌入大批任务的时候,才会把线程数加码到最大数量。
更多Java技术文章参见《Java开发笔记(序)章节目录》