一、线程池概述
线程经常用来同时处理一个程序的多个任务,但是在并发任务非常多并且处理时间短的情况下,使用线程就需要面临一个问题,假设我们把线程创建的时间看做T1,线程执行任务的时间看做T2,线程销毁的时间看做T3,学过小学数学的都知道,只有当T2的时间足够大时,这个线程才能执行更多的任务,而不是把时间花费在创建和销毁上。
然而实际开发中,很少能人为控制T2的时间,因此,如何缩减T1和T3的时间,就是目前的解决思路,也就是线程池对线程资源开销的解决思路。
线程池将一个或者多个线程多次反复利用,同时需要对并发任务进行处理(也就是在任务增加的情况下,动态的创建新的线程),同时也需要考虑在程序空闲的情况下,对资源的释放(也就是在没有新的任务的情况下,销毁已有的线程,释放占用的资源)。
二、线程池构造
通常我们通过Java提供的ThreadPoolExecutor类来配置一个线程池对象。
这个类重要的构造参数主要有以下几个:
1)corePoolSize:核心线程数
该参数决定线程池能够一直活跃的线程数量。每当一个任务交给线程池,只要当前核心线程数小于corePoolSize,那么就会新建一个线程运行新来的任务。
2)BlockingQueue<Runnable> workQueue:等待队列
队列一般分为有界队列和无界队列(无界队列因为会无限扩容的原因,一旦处理速度跟不上任务产生速度,任务堆积,就会导致内存膨胀溢出)。
当核心线程数已经达到了设定大小时,新来的任务会交给等待队列。
3)maximumPoolSize:最大线程数
当等待队列也满了的时候,线程池会进入996加班模式,也就是会继续new Thread来处理新来的任务(如果设置的maximumPoolSize==corePoolSize,那么这个线程池不会创建新的线程。)
4)RejectedExecutionHandler handler:拒绝执行策略
当一个任务被拒绝执行时(比如线程池和等待队列都爆满的情况下),会调用handler的rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法(如果有的话)。
5)keepAliveTime:保持活跃时间
这个参数主要用来限制非核心线程的存活时间。 确保不会有线程长时间进入空闲状态,占用资源。
但是该参数对核心线程无效,核心线程一旦被创建,除非人为关闭,否则一直存活。
6)TimeUnit timeUnit:keepAliveTime的单位
该参数是枚举类型,用来指定keepAliveTime的时间单位(秒,毫秒……),可以赋值TimeUnit.SECONDS表示秒。
7)ThreadFactory threadFactory:线程工厂
顾名思义,该对象主要用来创建线程对象。(JDK有提供默认的线程创建模式。)
三、线程池的创建
在二的基础之上,我们就可以开始构造适用于自己的线程池了。
为了演示线程创建和拒绝策略的执行,其中ThreadFactory ,RejectedExecutionHandler的实现需要自己手写一下。
在这里我先写好一个任务,继承了Runable接口
//因为是内部类所以加了static,如果不写成内部类,就不需要写
static class MyTask implements Runnable {
private String taskName;
public MyTask(String taskName) {
this.taskName = taskName;
System.out.println(getTaskName() + "is create");
}
@Override
public void run() {
System.out.println(getTaskName() + "is running");
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getTaskName() + "is over");
}
public String getTaskName() {
return "任务"+taskName;
}
@Override
public String toString() {
return "任务"+taskName;
}
}
接着实现一下线程池中ThreadFactory的创建
static class MyThreadFactory implements ThreadFactory {
private int threadNo = 0;
@Override
public Thread newThread(Runnable r) {
//线程池会调用该方法来创建线程对象
++threadNo;
System.out.println("Thread" + threadNo + "is created");
return new Thread(r,String.valueOf(threadNo));
}
}
接着准备一下拒绝执行策略
static class MyRejectedHandler implements RejectedExecutionHandler {
//线程池在拒绝一个任务的请求之后,会调用该方法
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
MyTask mt = (MyTask) r;
//获取此时的队列
BlockingQueue<Runnable> workQueue = executor.getQueue();
System.err.print(mt.getTaskName() + " is rejected,原因:");
System.err.print("等待队列当前大小:"+workQueue.size() + "已经达到最大值,");
//获取迭代器循环遍历所有的任务,取出任务名并且打印
Iterator<Runnable> iterator = workQueue.iterator();
String message = "";
while(iterator.hasNext()){
message += iterator.next().toString()+" ";
}
System.err.println("等待队列的任务是"+message);
}
}
现在万事俱备,可以创建线程池了
public static void main(String[] args) {
//核心线程数
int corePoolSize = 2;
//最大线程数
int maximumPoolSize = 4;
//保持生命时间:为了节约资源,当一个线程空闲时间超过keepAliveTime时,该线程会被注销(默认对核心线程无效)
long keepAliveTime = 2;
//keepAliveTime的时间单位,属于枚举类型
TimeUnit timeUnit = TimeUnit.SECONDS;
//等待队列:构造函数表示队列的大小,当所有的核心线程都在忙碌时,新来的任务会被存储在此
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);
//线程创建工厂:工厂设计模式,用来构建线程对象
ThreadFactory threadFactory = new MyThreadFactory();
//拒绝执行后处理器:当等待队列已满,新来的任务会被拒绝执行,此时会调用处理器的rejectedExecution方法来处理异常
RejectedExecutionHandler handler = new MyRejectedHandler();
//创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, threadFactory, handler);
//创建核心线程
int coreNo = threadPoolExecutor.prestartAllCoreThreads();
System.out.println(coreNo+"个核心线程被创建");
//循环创建任务
for (int i=1;i<=10;i++){
threadPoolExecutor.execute(new MyTask(String.valueOf(i)));
}
//程序执行之后不会自己停止,因为线程池还挂着呢。需要手动释放线程才能结束程序。
}
最后看一下运行结果吧。
可以看到,一开始手动创建了所有的核心线程(空闲状态),之后开始循环创建任务。
接着任务1和任务2因为此时核心线程空闲,直接运行,任务3和任务4则在等待队列中等待。
之后又创建了任务5,此时核心线程和等待队列已经满了,但是线程数还没有达到最大,因此创建了新的线程3来执行这个新任务。
任务6和任务5的待遇相同。因为线程的创建需要时间,因此任务的运行和创建之间等待的时间较长。
任务7,8,9,10因为此时的线程数已经达到最大,等待队列也爆满,因此,全部都被拒绝执行并且执行了拒绝策略。
接着,任务1和任务2执行完毕,等待队列的任务3和任务4继续执行。
最后所有的任务都执行完毕,程序……还没停。
需要手动销毁线程池之后,程序才会正常关闭
四、线程池的关闭
一般情况下,线程池都是为了解决并发项目的任务而开启的,所以并不需要关闭线程池。但是如果想要关闭线程池的话,由于核心线程并不会自己销毁,因此需要手动结束线程,线程池想要自动销毁的话,需要满足两个条件。
- 线程池引用不可达。
- 线程池的线程数为0。
推荐通过调用线程池提供的方法来关闭线程。
主要有以下几个方法:
- shutdown():该方法会准备关闭线程池,执行此方法,会等待所有任务结束之后,再关闭线程池。
- shutdownNow():该方法会立刻关闭线程池,执行此方法,正在执行的任务会被强制中断。
- getActiveCount():该方法会返回当前正在工作的线程数,当所有的线程都处于空闲状态时,返回值为0。
当线程池被关闭之后,线程池将无法再执行新的任务,否则会抛出异常!(准确来说,会执行拒绝执行策略代码)。
五、线程关闭机制
虽然在之前提到,使用shutdownNow()可以立即关闭线程池,但是其实质上并不是如此。
追踪源码我们可以发现,线程池的shutdownNow()方法,本质上还是调用了线程本身的关闭方法interrupt()。所以,shutdownNow()方法也不能保证立即关闭线程池。
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
//shutdownNow()的底层实现是调用了线程对象的interrupt方法
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
因为线程的关闭方法interrupt() 其本质上并没有关闭线程,能够直接关闭线程的stop()等方法都因为线程安全问题而废弃。interrupt()方法仅仅只是更改了线程对象的一个标志位,告诉这个线程,应该要关闭了,但是实际上是否关闭,取决于线程本身。因此,即便使用showdownNow()方法之后,线程池中的线程关闭还是不关闭,也要取决于线程自身。。。
这里根据应用场景有多个不同解决方案,如果任务是经常做循环或者递归操作,那么可以用这样的方式:
@Override
public void run() {
for (int i=0;i<1000000;i++){
//在方法体中添加校验关闭标志位,判断线程是否应该关闭
if(Thread.interrupted()){
System.out.println("退出线程"+i);
break;
}else{
i++;
}
}
}
这个程序在使用线程池运行,并且使用shutdownNow()关闭后,结果如下
可以发现,该任务的循环并没有结束,线程就被关闭了。
如果去掉校验,改成这样:
@Override
public void run() {
int i = 0;
for (;i<1000000;i++){
//在方法体中去掉标志位
i++;
}
System.out.println("退出线程"+i);
}
运行结果:
即便使用了shutdownNow(),线程依旧做完了循环才关闭,这样看来在多次循环中如果不对标志位认为判断,shutdownNow()和shutdown()几乎没什么区别了。