ThreadPoolExecutor是Java语言对于线程池的实现。池化技术是一种复用资源,减少开销的技术。线程是操作系统的资源,线程的创建与调度由操作系统负责,线程的创建与调度都要耗费大量的资源,其中线程创建需要占用一定的内存,而线程的调度需要不断的切换线程上下文造成一定的开销。同时线程执行完毕之后就会被操作系统回收,这样在高并发情况下就会造成系统频繁创建线程。

为此线程池技术为了解决上述问题,使线程在使用完毕后不回收而是重复利用。如果线程能够复用,那么我们就可以使用固定数量的线程来解决并发问题,这样一来不仅节约了系统资源,而且也会减少线程上下文切换的开销。

参数

ThreadPoolExecutor的构造函数有7个,它们分别是:

  1. corePoolSize(int):线程池的核心线程数量
  2. maximumPoolSize(int):线程池最大线程数量
  3. keepAliveTime(long):保持线程存活的时间
  4. unit(TimeUnit):线程存活时间单位
  5. workQueue(BlockingQueue):工作队列,用于临时存放提交的任务
  6. threadFactory(ThreadFactory):线程工厂,用于创建线程
  7. handler(RejectedExecutionHandler):任务拒绝处理器,当线程池无法再接受新的任务时,会交给它处理

一般情况下,我们只使用前五个参数,剩余两个我们使用默认参数即可。

任务提交逻辑

其实,线程池创建参数都与线程池的任务提交逻辑密切相关。根据源码描述可以得知:当提交一个新任务时(执行线程池的execute方法)会经过三个步骤的处理。

  1. 当任务数量小于corePoolSize时,线程池会创建一个新的线程(创建新线程由传入参数threadFactory完成)来处理任务,哪怕线程池中有空闲线程,依然会选择创建新线程来处理
  2. 当任务数量大于corePoolSize时,线程池会将新任务压入工作队列(参数中传递的workQueue)等待调度。
  3. 当新提交的任务无法压入工作队列时,会检查当前任务数量是否大于maximumPoolSize。如果小于maximunPoolSize则会新建线程来处理任务(这时我们的keepAliveTime参数就起作用了,它主要作用于这种情况下创建的线程,如果任务数量减小,这些线程闲置了,那么在超过keepAliveTime时间后就会被回收)。如果大于了maximumPoolSize就会交由任务拒绝处理器handler处理。

线程池状态

正如线程有不同的状态一样,线程池也拥有不同的运行状态。源码中提出,线程池有五种状态,分别为:

  1. RUNNING:运行状态,不断接收任务并处理它们。
  2. SHUTDOWN:关闭状态,不接收新任务,但是会处理工作队列中排队的任务。
  3. STOP:停止状态,不接收新任务,清空工作队列且不会处理工作队列的任务。
  4. TIDYING:待终止状态,此状态下,任务队列和线程池都为空。
  5. TERMINATED:终止状态,线程池关闭。

如何让线程不被销毁

文章开头说到,线程在执行完毕之后会被操作系统回收销毁,那么线程池时如何保障线程不被销毁?首先看一个测试用例:

public static void testThreadState()
{
Thread thread = new Thread(() -> System.out.println("Hello world")); // 创建一个线程
System.out.println(thread.getState()); // 此时线程的状态为NEW
thread.start(); // 启动线程,状态为RUNNING
System.out.println(thread.getState());
try
{
thread.join();
System.out.println(thread.getState()); // 线程运行结束,状态为TERMINATED
thread.start(); // 此时再启动线程会发生什么呢?
} catch (InterruptedException e)
{
e.printStackTrace();
}
}

结果输出:

NEW
RUNNABLE
Hello world
TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
at java.base/java.lang.Thread.start(Thread.java:794)
at misc.ThreadPoolExecutorTest.testThreadState(ThreadPoolExecutorTest.java:90)
at misc.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:114)

可以看出,当一个线程运行结束之后,我们是不可能让线程起死回生重新启动的。既然如此ThreadPoolExecutor如何保障线程执行完一个任务不被销毁而继续执行下一个任务呢?

其实这里就要讲到我们最开始传入的参数workQueue,它的接口类型为BlockingQueue<T>,直译过来就是阻塞队列。这中队列有个特点,就是当队列为空而尝试出队操作时会阻塞

基于阻塞队列的如上特点,ThreadPoolExecutor采用不断循环+阻塞队列的方式来实现线程不被销毁。

 final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 从工作队列中不断取任务。如果工作队列为空,那么程序会阻塞在这里
while (task != null || (task = getTask()) != null) {
w.lock();
// 检查线程池状态
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
try {
//// 执行任务 ////
task.run();
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

关闭线程池

想要关闭线程池可以通过调用shutdown()shutdownNow()方法实现。两种方法有所不同,其中调用shutdown()方法会停止接收新的任务,处理工作队列中的任务,调用这个方法之后线程池会进入SHUTDOWN状态,此方法无返回值并且不抛出异常。

shutdownNow()方法会停止接收新的任务,而且会返回未完成的任务集合,同时这个方法也会抛出异常。

如何创建一个适应业务背景的线程池

线程池创建有七个参数,这几个参数的相互作用可以创建出适应特定业务场景的线程池。其中最为重要的有三个参数分别为:corePoolSizemaximumPoolSizeworkQueue。其中前两个参数已经在上文中作了详细介绍,而workQueue参数在线程池创建中也极为重要。workQueue主要有三种:

  1. SynchronousQueue:这个队列只能容纳一个元素,而且只有当队列为空时可以入队。
  2. ArrayBlockingQueue:这是一个固定容量大小的队列。
  3. LinkedBlockingQueue:链式阻塞队列,容量无限。

通过上述三种队列的特性我们可以得知,

  1. 当使用SynchronousQueue的时候,总是倾向于新建线程处理请求,如果线程池大小参数设置的很大,那么线程数量倾向于无限增长。这样的线程池能够高效处理突发增长的请求,而且处理效率很高,但是开销很大。
  2. 当使用ArrayBlockingQueue的时候,线程池所能处理的瞬时最大任务量为队列大小 + 线程池最大数量,这样的线程池中规中矩,使用的业务场景很多,具体还需结合业务场景来调配三个参数的大小。例如I/O密集型的场景,多数的线程处于阻塞状态,为了提高系统吞吐量,我们希望能够有多数线程来处理IO。这样的话我们偏向于将corePoolSize设置的大一点。而且阻塞队列大小不要设置很大,同时maximumPoolSize也设置的大一点。
  3. 当使用LinkedBlockingQueue时,线程池的maximumPoolSize参数会失效,因为按照任务提交流程来看,LinkedBlockingQueue可以无限制地容纳任务,自然不会出现队列无法工作,新建线程处理的情况。使用LinkedBlockingQueue可以处理平稳的处理一些请求激增的情况,但是处理效率不会提高,仅仅能够起到一定的缓冲作用。

Java ThreadPoolExecutor详解的更多相关文章

  1. Java内部类详解

    Java内部类详解 说起内部类这个词,想必很多人都不陌生,但是又会觉得不熟悉.原因是平时编写代码时可能用到的场景不多,用得最多的是在有事件监听的情况下,并且即使用到也很少去总结内部类的用法.今天我们就 ...

  2. 黑马----JAVA迭代器详解

    JAVA迭代器详解 1.Interable.Iterator和ListIterator 1)迭代器生成接口Interable,用于生成一个具体迭代器 public interface Iterable ...

  3. C++调用JAVA方法详解

    C++调用JAVA方法详解          博客分类: 本文主要参考http://tech.ccidnet.com/art/1081/20050413/237901_1.html 上的文章. C++ ...

  4. Java虚拟机详解----JVM常见问题总结

    [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...

  5. [转] Java内部类详解

    作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本博客中未标明转载的文章归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置 ...

  6. Java面向对象详解

    Java面向对象详解 前言:接触项目开发也有很长一段时间了,最近开始萌发出想回过头来写写以前学 过的基础知识的想法.一是原来刚开始学习接触编程,一个人跌跌撞撞摸索着往前走,初学的时候很多东西理解的也懵 ...

  7. java 乱码详解_jsp中pageEncoding、charset=UTF -8"、request.setCharacterEncoding("UTF-8")

    http://blog.csdn.net/qinysong/article/details/1179480 java 乱码详解__jsp中pageEncoding.charset=UTF -8&quo ...

  8. java 泛型详解(普通泛型、 通配符、 泛型接口)

    java 泛型详解(普通泛型. 通配符. 泛型接口) JDK1.5 令我们期待很久,可是当他发布的时候却更换版本号为5.0.这说明Java已经有大幅度的变化.本文将讲解JDK5.0支持的新功能---- ...

  9. Java synchronized 详解

    Java synchronized 详解 Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 1.当两个并发线程访问同一个对象object ...

随机推荐

  1. 记一次,Docker镜像1G多精简至300+M的过程

    记一次,Docker镜像1G多精简至300+M的过程 一.业务场景描述 二.Docker时区不一致,相差8小时 三.通过Docker发布的服务上传文件上传不上去 四.上传的图片带水印,水印中包含的字体 ...

  2. EXCEL序列

  3. 说说 JavaScript中 call和apply

    下面有关JavaScript中 call和apply的描述,错误的是? call与apply都属于Function.prototype的一个方法,所以每个function实例都有call.apply属 ...

  4. Codeforces Global Round 9 D. Replace by MEX

    题目链接:https://codeforces.com/contest/1375/problem/D 题意 给出一个大小为 $n$,元素值位于 $[0,n]$ 之间的数组,每次可以将一个元素替换为数组 ...

  5. 2020牛客暑期多校训练营(第二场)Fake Maxpooling

    传送门:Fake Maxpooling 题意:给出矩阵的行数n和列数m,矩阵 Aij = lcm( i , j )  ,求每个大小为k*k的子矩阵的最大值的和. 题解:如果暴力求解肯定会t,所以要智取 ...

  6. POJ 3281 Dining(最大流板子)

    牛是很挑食的.每头牛都偏爱特定的食物和饮料,其他的就不吃了. 农夫约翰为他的牛做了美味的饭菜,但他忘了根据它们的喜好检查菜单.虽然他不可能喂饱所有的人,但他想让尽可能多的奶牛吃上一顿有食物和水的大餐. ...

  7. CF1475-C. Ball in Berland

    CF1475-C. Ball in Berland 题意: 一个班级有\(a\)个男生和\(b\)个女生,现在这个班级有\(k\)对男女愿意一起出席毕业典礼,这里注意\(k\)对男女中可能会有某个男生 ...

  8. 包机制和javadoc

    包机制 一般用公司域名倒置作为包名: eg com.baidu.www 可以在src里自己鼠标右键建立包,包内可以存放代码, 包的路径必须在最上面, eg:package com.kuang; (自动 ...

  9. Windows10电脑优化和使用

    本文将结合自身经验和短视频软件中的优化技巧,推荐一些Win10系统的优化和使用小技巧. 电脑优化 新电脑调出我的电脑等桌面图标: 右键桌面,选择个性化,左侧选择主题,在相关的设置中找到桌面图标设置,将 ...

  10. nyoj-1236 挑战密室

    挑战密室 时间限制:1 s | 内存限制:128 M 提交 状态 排名 题目描述 R组织的特工Dr. Kong 为了寻找丢失的超体元素,不幸陷入WTO密室.Dr. Kong必须尽快找到解锁密码逃离,否 ...