前言
昨天我们分享了线程池的相关知识点,我们先做一个简单回顾,昨天的内容主要是围绕线程池的构造方法,解释了各个参数的作用,以及如何定义一个线程池,最后我们通过一段示例代码,展示了各个参数的作用,同时也演示了不同参数下线程池运行状态情况,最终我们得出的结论是:
线程池能够处理的最大任务数是corePoolSize + maximumPoolSize + workQueue.size()
但是由于时间原因,昨天还有一些知识点没来得及分享,所以今天我们来继续看下剩余的内容。
线程池
昨天我们说如果不设定workQueue
的大小,那永远都不会报拒绝这个错误,当然maximumPoolSize
也就无效了,今天我们就先来演示下这个问题
代码和昨天的类似:
int corePoolSize = 10;
int maximumPoolSize = 20;
long keepAliveTime = 1000;
TimeUnit unit = TimeUnit.MICROSECONDS;
BlockingDeque<Runnable> workQueue = new LinkedBlockingDeque<>();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
for (int i = 0; i < 80; i++) {
System.out.println(i + " # " + threadPoolExecutor.toString());
threadPoolExecutor.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println("hello threadPool: "+ name);
});
}
threadPoolExecutor.shutdown();
}
只是我们现在的工作队列是没有指定大小的,其他参数和昨天是一样的,循环次数依然是80
次,然后我们运行下:
这时候虽然没有线程池拒绝的错误,但是我们发现虽然最开始设定的最大线程数是20
,但是在整个运行过程中,不论工作队列有多大,线程池始终只有10
个线程,说明最大线程数失效了;
不过这一点也很容易理解,因为最大线程数只有在工作队列排满的时候才会生效,现在我们没有设定工作队列的大小,也就是大小无上限,这样工作队列永远不会满,所以永远都不会触发最大线程的设定。
线程存活时间
keepAliveTime
这个参数昨天忘记讲了,这个参数是设定线程的存活时间,如果任务很多,并且每个任务执行时间比较短的话,调大这个参数可以提高线程的利用率。
官方文档中给出的解释是keepAliveTIme
表示当线程数大于核心时,多余空闲线程在终止前等待新任务的最长时间。
需要注意的是,这个参数主要是设定工作队列中的线程存活时间,从系统源码中就可以看出这一点:
在获取任务的时候,会从工作队列中拿出并删除(poll
)一个runnable
线程,poll
的方法实现会根据存活时间做业务处理,但是根据系统代码,我发现这个参数只有当工作队列为空的时候才会起作用,这也就是说这个参数其实就是让线程在处理完所有任务(工作队列中没有任务)之后,等等我们设定的存活时间,然后再销毁。
那我们是不是可以根据官方的解释以及keepAliveTime
大胆猜测,多余线程(超过基本线程数的线程)是在工作队列为空的时候开始销毁的,因为这样这个存活时间才有意义,因为核心线程从初始化之后会一直存活
下面我们把代码简单修改下,验证下我们的推论:
int corePoolSize = 10;
int maximumPoolSize = 30;
long keepAliveTime = 20000;
TimeUnit unit = TimeUnit.MICROSECONDS;
BlockingDeque<Runnable> workQueue = new LinkedBlockingDeque<>(170);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
CountDownLatch countDownLatch = new CountDownLatch(200);
for (int i = 0; i < 200; i++) {
System.out.println(i + " # " + threadPoolExecutor);
threadPoolExecutor.execute(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println("hello threadPool: "+ name);
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("循环完成,现在的线程池状态 # " + threadPoolExecutor);
Thread.sleep(10000);
System.out.println("循环完成,休眠10秒线程池状态 # " + threadPoolExecutor);
Thread.sleep(10000);
System.out.println("循环完成,休眠20秒线程池状态 # " + threadPoolExecutor);
threadPoolExecutor.shutdown();
我们增加了几行打印,同时也把循环次数、工作队列大小和存活时间调大,增加countDownLatch
主要是控制线程执行顺序,还增加了一个睡眠时间,然后运行:
虽然结果和我们预期的差不多,但还是有出入,根据运行结果,我们发现从执行完成的第10
秒,线程池的大小就变成了10
,也就是我们的核心线程数量,当然这结果是没有问题的,因为存活时间是从任务分发完成(也就是工作队列为空)开始算起的,分发过程肯定很快,而且那时候好多线程还没有开始运行,所以运行完成休眠10
秒就达到了线程的存活时间,然后多余的线程就被销毁了。
关闭线程池
关闭线程池很简单,只需要在程序结尾执行如下代码即可:
threadPoolExecutor.shutdown();
但是这个关闭并不是立即生效,首先这个方法会检查是否有关闭权限,然后才能提交关闭操作,尽管这个样,它也要等待所有线程执行完成后才能关闭,哪怕你是直接在线程内部执行关闭操作:
运行结果:
从结果中我们可以看出来,虽然在线程内部关闭了线程池,但是线程池还是等到所有线程执行完成后才关闭,而且在线程池关闭前,线程资源的释放是逐步进行的,而不是最后才释放。
如果直接在execute()
方法前执行线程池关闭命令,execute()
方法在运行的时候就会报错:
需要注意的是,线程池使用完之后,必须要关闭,否则当前方法会被阻塞
就像上面这样,虽然方法执行完成了,但是主线程并未退出,线程池一直在等待新的任务进来,所以资源一直无法释放。
总结
今天我发现又讲不完了