多线程之线程池(中)

云中志

共 5182字,需浏览 11分钟

 ·

2021-07-17 17:53

前言

昨天我们分享了线程池的相关知识点,我们先做一个简单回顾,昨天的内容主要是围绕线程池的构造方法,解释了各个参数的作用,以及如何定义一个线程池,最后我们通过一段示例代码,展示了各个参数的作用,同时也演示了不同参数下线程池运行状态情况,最终我们得出的结论是:

线程池能够处理的最大任务数是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()方法在运行的时候就会报错:

需要注意的是,线程池使用完之后,必须要关闭,否则当前方法会被阻塞

就像上面这样,虽然方法执行完成了,但是主线程并未退出,线程池一直在等待新的任务进来,所以资源一直无法释放。

总结

今天我发现又讲不完了,不过没关系,分三次讲可以更详细一点。今天我们主要分享了三个知识点:

  • 不设定workQueue大小,maximumPoolSize参数失效问题
  • 线程池线程存活时间的分析探讨
  • 线程关闭问题

同时也针对这三个知识点做出了简单论证,通过今天的内容,可以让我们对线程池的生命周期有更深入更全面的了解和认识,明天我们会分享线程池的拒绝策略、线程工厂等相关知识,同时也会有一些补充内容,后天应该会就线程池这块做一个小结,再后面就是多线程部分的查漏补缺和总结了,好了,今天就先到这里吧!

- END -


浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报