Java并发编程:线程池的使用

前言

一提到线程池,我们都容易想到高并发各种脑补场景,其实高并发是一个抽象的概念,要实现高并发并不仅仅是一个Java线程集合类,或者Java基础层面就能搞定的事情。这其中涉及到方方面面,从前端到后端,到支持高并发的中间组件,最后到数据存储,持久化层面等等都需要对高并发设计和考量。因此,前方的道路是漫长且艰难的,只有让我们对技术保持着敬畏之心,对学习保持热情态度,对编程保持热爱之情,一点一滴的学习和沉淀这些知识和技能,终有一天你会把这些东西由点到线,由线到面汇聚属于自己的知识脉络,形成自己的知识体系,蜕变成当年青春年少时渴望成为的大神。

线程池初体验

在不用线程池之前,我们一般是通过new Thread(Runanble r).start()来启动一个线程的,这种方法对单一任务的执行很方便,但是由于每个任务都必须创建一个线程,对大量的任务而言是不高效的。为每一任务开始一个新线程可能会限制流量并且造成性能的降低,因此线程池是管理并发执行任务个数的理想方法。

废话不多说,让我们先来体验一把线程池的好处,这里以并发的完成100个任务为例,体验一下使用线程池和不使用线程池有何区别。先来一下不用线程池的例子:

public class Demo {
	public static void main(String[] args) {
		//循环100次,启动任务
		for(int i = 0; i < 100; i ++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					System.out.println("ID : " + Thread.currentThread().getId() + ", Name:" + Thread.currentThread().getName()+"任务启动:");
				}
			}).start();
		}
	}
}

这个例子的Main主线程做了100次循环,每循环一次创建并启动一个线程,而每一个启动的线程做的唯一一件事就是打印一下自身的线程ID和自身线程名称,不出意外的话,你的控制台将输出如下:

ID : 9, Name:Thread-0任务启动:
ID : 10, Name:Thread-1任务启动:
ID : 11, Name:Thread-2任务启动:
...
ID : 107, Name:Thread-98任务启动:
ID : 108, Name:Thread-99任务启动:
ID : 106, Name:Thread-97任务启动:

上面我省略掉了中间部分输出,总之从控制台的打印输出中可以看到100个不同线程ID和线程名称,这说明这里用了100个线程来完成这100个任务。那么我们来看看线程池是怎么用的:

/**
 * 利用线程池来完成100个任务
 * @author 张仕宗
 * @date 2018.11.16
 */
public class Demo {
	public static void main(String[] args) {
		ExecutorService pool = Executors.newFixedThreadPool(10); //创建一个线程池
		
		//循环100次,启动任务
		for(int i = 0; i < 100; i ++) {
			pool.execute(new Runnable() {
				@Override
				public void run() {
					System.out.println("ID : " + Thread.currentThread().getId() + ", Name:" + Thread.currentThread().getName()+"任务启动:");					
				}
			});
		}
		pool.shutdown(); //关闭线程池
	}
}

上述代码比较少,也比较简洁,代码首先创建了一个可以容纳10个线程的线程池pool,然后在for循环中循环100次,每循环一次时,我们只需要将我们的线程对象通过pool.execute()方法提交给线程池即可,线程池会帮我们去调用线程的run()方法来启动线程,最后不要忘记使用pool.shutdown()来关闭我们的线程池。

下面我将控制台部分打印输出贴出来:

ID : 9, Name:pool-1-thread-1任务启动:
ID : 13, Name:pool-1-thread-5任务启动:
ID : 14, Name:pool-1-thread-6任务启动:
ID : 12, Name:pool-1-thread-4任务启动:
ID : 11, Name:pool-1-thread-3任务启动:
ID : 10, Name:pool-1-thread-2任务启动:
ID : 15, Name:pool-1-thread-7任务启动:
ID : 16, Name:pool-1-thread-8任务启动:
ID : 17, Name:pool-1-thread-9任务启动:
ID : 16, Name:pool-1-thread-8任务启动:
ID : 16, Name:pool-1-thread-8任务启动:
ID : 18, Name:pool-1-thread-10任务启动:
ID : 15, Name:pool-1-thread-7任务启动:
ID : 10, Name:pool-1-thread-2任务启动:

对比两次输出可以看到,在启动任务的过程中,用线程池这种方法会使用同一线程来完成多次任务,如上述控制台输出ID : 10, Name:pool-1-thread-2任务启动:正面ID为10这个线程就被叫去完成任务两次,同样的线程ID为15的哥们也被叫去完成两次任务。案例2与案例1不同的是:案例1中启动了100个线程去完成100个任务,每个线程完成任务之后线程的生命周期就结束。而案例2只启动了10个线程去完成100个任务,当它完成任务时,线程并没有消亡,而是把自己交给了线程池来调度。

不知道你看懂了上面两个例子了没有?如果你看懂了上面两个例子了,那么接下来你可能会疑惑了,说:博主,你明明用的是ExecutorService类的对象来完成这件事的,为什么非要把它叫做线程池呢?别着急,让我慢慢说来给你听:首先,那个ExecutorService是一个接口,我们都知道接口一般用来定义行为的,表示该接口能够做一些事情;其次,在这个案例中真正干活的是这个类ThreadPoolExecutor,这就是我们的线程池实现类,它实现了ExecutorService接口,需要注意的是这里的ExecutorService并不是*接口,它还有一个Executor父接口;最后,我们的例子中线程池并不是new创建出来的,而是用Executors工具类获取的,后面小节会讲到Executors工具类。

对上述使用线程池的案例,UML类图如下:
Java并发编程:线程池的使用

虽然Executor是*接口,但是它只定义了一个execute()方法,没有定义诸如shutdown()关闭线程池,isTermaed()判断线程池是否执行结束等方法,所以经常用ExecutorService子接口来完成我们的工作。Executors是一个工具类或者线程池框架,它预定义了线程池对线程的创建和使用方法以及调度方式,它的底层借助ThreadPoolExecutor线程池类来实现的功能。

ThreadPoolExecutor类

ThreadPoolExecutor类是线程池的核心类,它被重载的构造方法有很多,我们挑参数最全的一个构造方法来讲解线程池是怎么工作的。通过阅读源码可知,参数最全的构造方法如下:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) ;

参数说明:

  1. corePoolSize: 核心线程数,线程池初始化的时候就会被创建。
  2. maximumPoolSize:线程池的最大上限
  3. keepAliveTime:线程的存活时间
  4. unit:线程存活时间的单位,有时,分,秒,毫秒,天,周等单位。
  5. workQueue:工作队列用于在执行任务之前使用任务队列。
  6. threadFactory:执行器创建新线程时使用的线程工厂,定义了线程的创建方式。
  7. handler:线程池的拒绝策略。

下面我先来解释一下corePoolSize和maximumPoolSize参数的含义:ThreadPoolExecutor将根据corePoolSize和maximumPoolSize设置的边界自动调整池大小。当新任务在方法execute(java.lang.Runnable)中提交时,如果运行的线程小于等于corePoolSize,即使其他辅助线程是空闲的,也创建新线程来处理请求。如果运行的线程大于corePoolSize而小于等于maximumPoolSize时,新提交的任务加入队列。当队列已满,并且正在运行的线程数小于corePoolSize核心线程数时,创建新线程来处理请求。当队列已满,并且正在运行的线程数大于等于maximumPoolSize核心线程数时,执行handler拒绝策略的方法;如果设置的corePoolSize和maximumPoolSize相同,则创建了固定大小的线程池。如果将maximumPoolSize设置为基本的*值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。

handler拒绝策略,当任务源源不断的过来,而我们的系统又处理不过来的时候,我们要采取的策略是拒绝服务。RejectedExecutionHandler接口提供了拒绝任务处理的自定义方法的机会,可以自定义该接口的实现子类,也可以使用ThreadPoolExecutor已经定义好的实现类,ThreadPoolExecutor已经包含四种处理策略,分别是CallerRunsPolicy,AbortPolicy,DiscardPolicy和DiscardOldestPolicy,这四种策略是什么含义下面会再提。

keepAliveTime和unit,这是成对的参数,设置了线程在等待队列存活时间,含义如我上述所示。

看了上述的文字描述之后,不知道你是否是一脸蒙逼的进来,然后又一脸蒙逼的出去?别着急,俗话说千言万语不如一张图,下面是我画的ThreadPoolExecutor处理新线程的流程图:
Java并发编程:线程池的使用

上述图片是博主阅读JDK 1.6帮助文档和阅读源码理解勾画出来的,如有错误之处愿接受批评和指正。请仔细体会一下文字描述和上图的含义,为了帮助你理解,我用一个案例来深切体会一下其中的奥秘。

public class Task implements Runnable {
	
	private int taskId;
	
	public int getTaskId() {
		return taskId;
	}

	public void setTaskId(int taskId) {
		this.taskId = taskId;
	}

	public Task(int taskId) {
		this.taskId = taskId;
	}

	@Override
	public void run() {
		System.out.println("正在启动的taskId : " + this.taskId+",执行该任务的线程id:" + Thread.currentThread().getId());
		try {
			Thread.sleep((long)(Math.random()*3000));
		} catch(InterruptedException e) {
			e.printStackTrace();
		} finally {
			System.out.println("任务 ID :" + this.taskId+ "执行结束");
		}
	}

	@Override
	public String toString() {
		return "当前线程taskId : " + this.taskId;
	}
}

Task任务类,实现Runnable接口,是一个可用的线程,用来模拟做任务。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * ThreadPoolExecutor线程池的使用
 * @author 张仕宗
 * @date 2018.11.17
 */
public class TestThreadPoolExecutor {
	
	/**
	 *自定义线程工厂实现类
	 */
	private static class MyThreadFactory implements ThreadFactory {
		@Override
		public Thread newThread(Runnable r) {
			Thread thread = new Thread(r);
			if(thread.getPriority() != Thread.NORM_PRIORITY) {
				thread.setPriority(Thread.NORM_PRIORITY);//设置线程的优先级
			}
			if(thread.isDaemon()) {
				thread.setDaemon(false);//设置该线程为守护线程
			}
			return thread;
		}
	}
	
	/**
	 * 自定义拒绝策略
	 */
	private static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
		
		@Override
		public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
			System.out.println("当前任务被拒绝:" + r.toString());
		}
	}

	public static void main(String[] args) {
		BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);//创建一个有界的阻塞队列对象
		ThreadFactory factory = new MyThreadFactory(); //线程工厂
		RejectedExecutionHandler handler = new MyRejectedExecutionHandler(); //拒绝策略
		//创建一个线程池,并传递最全的参数
		ThreadPoolExecutor pool = new ThreadPoolExecutor(1, //corePoolSize,核心线程数
				2, //maximumPoolSize,线程池最大上线
				60, //keepAliveTime,线程存活时间
				TimeUnit.SECONDS, //unit,线程存活时间单位,秒
				workQueue, //workQueue,任务队列,有界阻塞队列
				factory,   //factory,线程工厂
				handler    //handler,拒绝策略
				);
		//创建2个线程
		Task t1 = new Task(1);
		Task t2 = new Task(2);
		
		//提交2个线程
		System.out.println("提交任务t1,目前线程池的状况:" + pool.toString());
		pool.execute(t1);
		System.out.println("提交任务t2,目前线程池的状况:" + pool.toString());
		pool.execute(t2);
		
		//关闭线程池
		pool.shutdown();
	}
}

TestThreadPoolExecutor类需要简单说明一下,该类定义了两个静态内部类MyThreadFactory和MyRejectedExecutionHandler,MyThreadFactory类实现了线程工厂,该线程工厂创建的新线程优先级都是普通级别的,并且设置新线程为守护线程。MyRejectedExecutionHandler拒绝策略实现类除了打印一句话之外什么都不做。main方法中创建了一个线程池pool,创建一个线程池工厂factory,一个handler,和两个线程对象,并通过pool.execute()提交这两个线程提交给线程池。Task类做的事情很比较简单,就是打印一下当前任务和做当前任务的线程ID。我们来看一下控制台的输出:

提交任务t1,目前线程池的状况:[email protected][Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
提交任务t2,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
正在启动的taskId : 1,执行该任务的线程id:9
任务 ID :1执行结束
正在启动的taskId : 2,执行该任务的线程id:9
任务 ID :2执行结束

由于主线程和线程池调用线程是并发关系的,所以可以理解为主线程一次性将t1,t2都提交给了线程池,当t1线程提交时,线程池的状态是:正在执行的任务0,队列中排队的任务也为0,完成的任务也是0,正在执行的任务<corePoolSize成立,所以创建一个新线程,新线程ID为9。当t2线程提交时,线程池的状态是:正在执行的任务个数为1,队列中排队的任务为0,正在执行的任务(个数1)<corePoolSize(个数1)不成立,队列中排队的个数(0) <maximumPoolSize(2)成立,所以第二任务进来之后进入队列等待。意思就是正在执行的任务个数已经达到了线程池的核心线程数,不能创建新线程来执行t2任务了,恰好排队队列还没满,让t2先去排队队列中等待。等t1任务执行结束,t2任务复用t1任务的线程(id号为9)来完成任务,所以控制台打印出两次任务提交都使用ID为9的线程。

如果上一个案例无法说明什么问题,下面我们将多增加一个任务,看看线程的执行顺序和运行情况:

	public static void main(String[] args) {
		BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);//创建一个有界的阻塞队列对象
		ThreadFactory factory = new MyThreadFactory(); //线程工厂
		RejectedExecutionHandler handler = new MyRejectedExecutionHandler(); //拒绝策略
		//创建一个线程池,并传递最全的参数
		ThreadPoolExecutor pool = new ThreadPoolExecutor(1, //corePoolSize,核心线程数
				2, //maximumPoolSize,线程池最大上线
				60, //keepAliveTime,线程存活时间
				TimeUnit.SECONDS, //unit,线程存活时间单位,秒
				workQueue, //workQueue,任务队列,有界阻塞队列
				factory,   //factory,线程工厂
				handler    //handler,拒绝策略
				);
		//创建3个线程
		Task t1 = new Task(1);
		Task t2 = new Task(2);
		Task t3 = new Task(3);
		
		//提交3个线程
		System.out.println("提交任务t1,目前线程池的状况:" + pool.toString());
		pool.execute(t1);
		System.out.println("提交任务t2,目前线程池的状况:" + pool.toString());
		pool.execute(t2);
		System.out.println("提交任务t3,目前线程池的状况:" + pool.toString());
		pool.execute(t3);
		
		//关闭线程池
		pool.shutdown();
	}

我只修改了main方法里面的代码,其他地方不做任何修改,控制台输出如下:

提交任务t1,目前线程池的状况:[email protected][Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
提交任务t2,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
提交任务t3,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]
正在启动的taskId : 1,执行该任务的线程id:9
任务 ID :1执行结束
正在启动的taskId : 2,执行该任务的线程id:9
任务 ID :2执行结束
正在启动的taskId : 3,执行该任务的线程id:9
任务 ID :3执行结束

任务t1和任务t2进来的情况和上面一样,只是当任务t3进来的时候,我们看看线程池的状态:

提交任务t3,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]

任务t3进来的时候,线程池里有一个任务t1在执行,而我们的corePoolSize刚好等于1,说明corePoolSize已满,maximumPoolSize为2说明此线程池能排队队列能排2个线程,此时只有t2一个任务在等待,说明队列还没满,可以继续加入一个先任务进来,则t3果断进入等待队列。那么,这三个任务的执行先后顺序是:t1加进来,创建新线程;t2加进来,进入等待队列,等t1执行结束复用t1中创建的线程去执行任务;t3加进来,进入等待队列,等t2执行结束复用t2用到的线程去执行任务,而执行t2任务用的线程就是t1创建的线程,所以t1,t2,t3三者共用了一个线程去执行3个任务。

那么看一下同时提交4个任务给线程池,是个什么情况?这里给出了main方法中的代码:

	public static void main(String[] args) {
		BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);//创建一个有界的阻塞队列对象
		ThreadFactory factory = new MyThreadFactory(); //线程工厂
		RejectedExecutionHandler handler = new MyRejectedExecutionHandler(); //拒绝策略
		//创建一个线程池,并传递最全的参数
		ThreadPoolExecutor pool = new ThreadPoolExecutor(1, //corePoolSize,核心线程数
				2, //maximumPoolSize,线程池最大上线
				60, //keepAliveTime,线程存活时间
				TimeUnit.SECONDS, //unit,线程存活时间单位,秒
				workQueue, //workQueue,任务队列,有界阻塞队列
				factory,   //factory,线程工厂
				handler    //handler,拒绝策略
				);
		//创建4个线程
		Task t1 = new Task(1);
		Task t2 = new Task(2);
		Task t3 = new Task(3);
		Task t4 = new Task(4);
		
		//提交4个线程
		System.out.println("提交任务t1,目前线程池的状况:" + pool.toString());
		pool.execute(t1);
		System.out.println("提交任务t2,目前线程池的状况:" + pool.toString());
		pool.execute(t2);
		System.out.println("提交任务t3,目前线程池的状况:" + pool.toString());
		pool.execute(t3);
		System.out.println("提交任务t4,目前线程池的状况:" + pool.toString());
		pool.execute(t4);
		
		//关闭线程池
		pool.shutdown();
	}

控制台输出如下:

提交任务t1,目前线程池的状况:[email protected][Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
提交任务t2,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
提交任务t3,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]
正在启动的taskId : 1,执行该任务的线程id:9
提交任务t4,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 2, completed tasks = 0]
正在启动的taskId : 4,执行该任务的线程id:10
任务 ID :1执行结束
正在启动的taskId : 2,执行该任务的线程id:9
任务 ID :4执行结束
正在启动的taskId : 3,执行该任务的线程id:10
任务 ID :2执行结束
任务 ID :3执行结束

这4个线程的执行很有意思,你可以对照着上述流程图和控制台输出来做一下分析。当t1任务进来的时候,正在运行的任务为0,所以直接创建新id号为9的新线程来执行任务t1,这一点是毋庸置疑的。t2任务进来的时候,已经有一个任务在执行,corePoolSize已满,然而队列为空,则t2进入等待队列,等待时间为多久由两个因素决定,第一个是t1的执行时间,第二个是等待时间是否超时,就是看那个keepAliveTime和unit参数值,如果t1执行超过60秒,则t2就放弃等待。t3任务进来的时候,t1还没有执行结束(注意可认为main方法向线程池提交4个任务的过程是瞬间的),corePoolSize已满,而此时队列等待的任务个数为1,而我们的线程池的maximumPoolSize为2,说明等待队列还没满,因此t3进入等待队列。而t3进入等待队列之后,队列的长度为2,等待队列已满。此时线程池的状态是:[Running, pool size = 1, active threads = 1, queued tasks = 2, completed tasks = 0]翻译过来的意思是:正在运行的线程个数为1(t1),等待队列中排队线程个数为2(t2,t3)。

此时,运行的线程已经达到corePoolSize的值,等待队列已经达到maximumPoolSize的值。就在这时,t4任务进来了,此时线程池怎么办呢?回顾一下我画的流程图!这种情况下线程池会判断运行的任务是否大于maximumPoolSize,如果不大于的话,直接创建新线程来运行t4;如果等待队列已满且正在运行的线程个数>=corePoolSize,那么将进入拒绝策略。现在t4满足等待队列已满,而正在运行的线程数小于maximumuPoolSize这种情况,所以此时线程池创建了id号为10的线程来执行任务t4。那么此时此刻线程池的状态是:t1(id为9)已经运行了一段时间了,还没结束,t2,t3在等待队列中等待,t4(id为10)创建了一个线程在运行,t1和t4在并行的执行着,t2和t3在等待。

上述状态维持不久,t1任务执行结束,将线程ID9释放出来,正在等待的t2任务拿到ID为9的线程去执行任务。此时t2和t4俩任务在并行的执行,t3在等待,t1已经执行完毕。不久t4也执行结束,t4比t2先执行结束,此时等待队列中的t3拿到t4任务的线程ID为10的线程去执行。此时t1,t4任务执行结束,t2拿到ID为9的线程正在执行,t3拿到ID为10的线程在执行。不多久t2和t3也运行结束,整个流程执行完毕,结果正如控制台输出的内容。

线程池提交4个任务的输出如上所述,当新增到5个的时候会发生什么事呢?main函数的代码我就不贴出了,只需要在你的代码加入下面这两句代码就行了:

Task t5 = new Task(5);
System.out.println("提交任务t5,目前线程池的状况:" + pool.toString());
pool.execute(t5);

控制台的输出如下:

提交任务t1,目前线程池的状况:[email protected][Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
提交任务t2,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
正在启动的taskId : 1,执行该任务的线程id:9
提交任务t3,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]
提交任务t4,目前线程池的状况:[email protected][Running, pool size = 1, active threads = 1, queued tasks = 2, completed tasks = 0]
提交任务t5,目前线程池的状况:[email protected][Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]
正在启动的taskId : 4,执行该任务的线程id:10
当前任务被拒绝:当前线程taskId : 5
任务 ID :1执行结束
正在启动的taskId : 2,执行该任务的线程id:9
任务 ID :4执行结束
正在启动的taskId : 3,执行该任务的线程id:10
任务 ID :2执行结束
任务 ID :3执行结束

与4个任务不同的是,当提交5个任务的时候,由于前4个线程中已有2个在队列中排队,t2个在执行,队列中任务个数和正在执行的个数都大于等于maximumPoolSize,此时新进来的t5被拒绝,因此控制台打印出

当前任务被拒绝:当前线程taskId : 5。

Executors框架

为了方面我们使用,J.U.C下的Executors类为我们提供常用场景下的线程池创建方式,不需要我们自己去操作JDK的ThreadPoolExecutor类。常用的创建线程池方法有:

  1. newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池。
  2. newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池,以共享的*队列方式来运行这些线程。
  3. newSingleThreadExecutor():创建一个使用单个 worker 线程的 Executor,以*队列方式来运行该线程。
  4. newScheduledThreadPool(int corePoolSize): 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

现在让我们回头过来看一下第二节线程池初体验中用到的线程池例子,该案例创建线程池的方法就是用newFixedThreadPool(int nThreads)这种方式来创建的。阅读Executors源码,发现该方法的实现代码是用ThreadPoolExecutor来实现的,Executors源码如下:

        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());

而Executors调用到的ThreadPoolExecutor构造方法源码又如下:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

该ThreadPoolExecutor构造方法调用了它自己参数最全的构造方法,factory用到了Execrutors默认的创建工厂,拒绝策略用了默认的解决策略。默认的决绝策略和上一节我们的实现没有什么区别,源码如下:

    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

newFixedThreadPool()这种创建线程池的方式第二节中用到了,我就不演示了,下面我来演示一下newCachedThreadPool(),这种方法会创建一个核心线程数为0,等待队列长度为Integer.MAX_VALUE整数型最大长度的队列,这说明使用这种线程池每个新进来的任务都会先进入队列,然后查看当前是否有空闲线程,有的话使用空闲线程,没有空闲线程则创建新线程,运行的线程个数和队列的长度都没有限制。测试用例如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 利用线程池来完成100个任务
 * @author 张仕宗
 * @date 2018.11.16
 */
public class Demo {
	public static void main(String[] args) {
		ExecutorService pool = Executors.newCachedThreadPool();
		
		//循环100次,启动任务
		for(int i = 0; i < 100; i ++) {
			pool.execute(new Runnable() {
				@Override
				public void run() {
					System.out.println("ID : " + Thread.currentThread().getId() + ", Name:" + Thread.currentThread().getName()+"任务启动:");					
				}
			});
		}
		pool.shutdown(); //关闭线程池
	}
}

输出节选如下:

ID : 9, Name:pool-1-thread-1任务启动:
ID : 10, Name:pool-1-thread-2任务启动:
ID : 11, Name:pool-1-thread-3任务启动:
ID : 12, Name:pool-1-thread-4任务启动:
...
ID : 12, Name:pool-1-thread-4任务启动:
ID : 39, Name:pool-1-thread-31任务启动:
ID : 46, Name:pool-1-thread-38任务启动:
ID : 36, Name:pool-1-thread-28任务启动:

我们看到有的线程被复用到了,但如果新进来的任务没有空闲线程给它使用,它会给它创建新的线程,所以这里一直增到了46号线程。

newSingleThreadExecutor()单例线程,返回以个包含单线程的Executor,将多个任务交给此Exector时,这个线程处理完一个任务后接着处理下一个任务,若该线程出现异常,将会有一个新的线程来替代。这种方法创建的线程池测试例子我就不赘述了,你需要将上面的测试用例中

ExecutorService pool = Executors.newCachedThreadPool();

换成下面这句代码就行了:

ExecutorService pool = Executors.newSingleThreadExecutor();

其他代码不用动,只需换掉一行代码,然后分析一下控制台的输出,顺便阅读一下Executors.newSingleThreadExecutor()实现的源码就OK了,相信阅读一段时间的源码,你写代码的能力也会跃上另一个台阶的,毕竟写JDK的作者都是大牛,而大牛写代码的思路是很值得我们学习的,是吧?

好了,Java并发编程系列的博客暂时写到这了,本来还想写Java并发编程的其他东西的,比如并发编程J.U.C一些工具类的使用和实现,Futrue设计模式,线程池的实现以及写一个Mini版web并发服务器等。但如前言所述,高并发的路途还很漫长,要深入学习的知识和技术还很多,博主要奔下一站Redis数据库而去了,希望能在Redis这一块Get到新技能并写出价值的文章出来,Java基础并发编程这一块如果后面学到新东西再把文章补上。

另,如果这篇文章如果对你有用,又或许你从中学到一些知识或受到一些启发,不要忘记了点赞哦,你的肯定和赞扬将会是我不断前进的动力。