Java 线程池详解
Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。
Executors创建线程池便捷方法列表:
| 方法名 | 功能 |
|---|---|
| newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
| newSingleThreadExecutor() | 创建只有一个线程的线程池 |
| newCachedThreadPool() | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 |
小程序使用这些快捷方法没什么问题,对于服务端需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor的构造方法。没错,上述Executors方法创建的线程池就是ThreadPoolExecutor。
ThreadPoolExecutor构造方法
Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:
|
1
2
3
4
5
6
7
8
9
|
// Java线程池的完整构造函数public ThreadPoolExecutor( int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。 int maximumPoolSize, // 线程数的上限 long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长, // 超过这个时间,多余的线程会被回收。 BlockingQueue<Runnable> workQueue, // 任务的排队队列 ThreadFactory threadFactory, // 新线程的产生方式 RejectedExecutionHandler handler) // 拒绝策略 |
竟然有7个参数,很无奈,构造一个线程池确实需要这么多参数。这些参数中,比较容易引起问题的有corePoolSize, maximumPoolSize, workQueue以及handler:
corePoolSize和maximumPoolSize设置不当会影响效率,甚至耗尽线程;workQueue设置不当容易导致OOM;handler设置不当会导致提交任务时抛出异常。
正确的参数设置方式会在下文给出。
线程池的工作顺序
If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.
corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略
Runnable和Callable
可以向线程池提交的任务有两种:Runnable和Callable,二者的区别如下:
- 方法签名不同,
void Runnable.run(),V Callable.call() throws Exception - 是否允许有返回值,
Callable允许有返回值 - 是否允许抛出异常,
Callable允许抛出异常。
Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。
三种提交任务的方式:
| 提交方式 | 是否关心返回结果 |
|---|---|
Future<T> submit(Callable<T> task) |
是 |
void execute(Runnable command) |
否 |
Future<?> submit(Runnable task) |
否,虽然返回Future,但是其get()方法总是返回null |
如何正确使用线程池
避免使用无界队列
不要使用Executors.newXXXThreadPool()快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:
|
1
2
3
4
|
ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM new ThreadPoolExecutor.DiscardPolicy()); |
明确拒绝任务时的行为
任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,接口定义如下:
|
1
2
3
|
public interface RejectedExecutionHandler { void rejectedExecution(Runnable r, ThreadPoolExecutor executor);} |
线程池给我们提供了几种常见的拒绝策略:

| 拒绝策略 | 拒绝行为 |
|---|---|
| AbortPolicy | 抛出RejectedExecutionException |
| DiscardPolicy | 什么也不做,直接忽略 |
| DiscardOldestPolicy | 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置 |
| CallerRunsPolicy | 直接由提交任务者执行这个任务 |
线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。
|
1
2
3
4
|
ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(512), new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略 |
获取处理结果和异常
线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionException,submit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
ExecutorService executorService = Executors.newFixedThreadPool(4);Future<Object> future = executorService.submit(new Callable<Object>() { @Override public Object call() throws Exception { throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者 } }); try { Object result = future.get();} catch (InterruptedException e) { // interrupt} catch (ExecutionException e) { // exception in Callable.call() e.printStackTrace();} |
上述代码输出类似如下:

线程池的常用场景
正确构造线程池
|
1
2
3
4
5
6
7
|
int poolSize = Runtime.getRuntime().availableProcessors() * 2;BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(512);RejectedExecutionHandler policy = new ThreadPoolExecutor.DiscardPolicy();executorService = new ThreadPoolExecutor(poolSize, poolSize, 0, TimeUnit.SECONDS, queue, policy); |
获取单个结果
过submit()向线程池提交任务后会返回一个Future,调用V Future.get()方法能够阻塞等待执行结果,V get(long timeout, TimeUnit unit)方法可以指定等待的超时时间。
获取多个结果
如果向线程池提交了多个任务,要获取这些任务的执行结果,可以依次调用Future.get()获得。但对于这种场景,我们更应该使用ExecutorCompletionService,该类的take()方法总是阻塞等待某一个任务完成,然后返回该任务的Future对象。向CompletionService批量提交任务后,只需调用相同次数的CompletionService.take()方法,就能获取所有任务的执行结果,获取顺序是任意的,取决于任务的完成顺序:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void solve(Executor executor, Collection<Callable<Result>> solvers) throws InterruptedException, ExecutionException { CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 构造器 for (Callable<Result> s : solvers)// 提交所有任务 ecs.submit(s); int n = solvers.size(); for (int i = 0; i < n; ++i) {// 获取每一个完成的任务 Result r = ecs.take().get(); if (r != null) use(r); }} |
单个任务的超时时间
V Future.get(long timeout, TimeUnit unit)方法可以指定等待的超时时间,超时未完成会抛出TimeoutException。
多个任务的超时时间
等待多个任务完成,并设置最大等待时间,可以通过CountDownLatch完成:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public void testLatch(ExecutorService executorService, List<Runnable> tasks) throws InterruptedException{ CountDownLatch latch = new CountDownLatch(tasks.size()); for(Runnable r : tasks){ executorService.submit(new Runnable() { @Override public void run() { try{ r.run(); }finally { latch.countDown();// countDown } } }); } latch.await(10, TimeUnit.SECONDS); // 指定超时时间 } |
线程池和装修公司
以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。
线程池就是程序中的“装修公司”,代劳各种脏活累活。上面的过程对应到线程池上:
|
1
2
3
4
5
6
7
8
|
// Java线程池的完整构造函数public ThreadPoolExecutor( int corePoolSize, // 正式工数量 int maximumPoolSize, // 工人数量上限,包括正式工和临时工 long keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇 BlockingQueue<Runnable> workQueue, // 排期队列 ThreadFactory threadFactory, // 招人渠道 RejectedExecutionHandler handler) // 拒单方式 |
总结
Executors为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch完成。
参考
Java 线程池详解的更多相关文章
- Java线程池详解(二)
一.前言 在总结了线程池的一些原理及实现细节之后,产出了一篇文章:Java线程池详解(一),后面的(一)是在本文出现之后加上的,而本文就成了(二).因为在写完第一篇关于java线程池的文章之后,越发觉 ...
- Java线程池详解
一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...
- 【java线程系列】java线程系列之java线程池详解
一线程池的概念及为何需要线程池: 我们知道当我们自己创建一个线程时如果该线程执行完任务后就进入死亡状态,这样如果我们需要在次使用一个线程时得重新创建一个线程,但是线程的创建是要付出一定的代价的,如果在 ...
- Java线程池详解,看这篇就够了!
构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?Runnable和Callable的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片段. 基础知识 Execu ...
- Java线程池 详解(图解)
来源:www.jianshu.com/p/098819be088c 拓展: 手动创建 new ThreadPoolExecutor 的使用: https://segmentfault.com/a/11 ...
- Java线程池详解(一)
一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...
- Java线程池详解及实例
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/aa1215018028/article/ ...
- 三、VIP课程:并发编程专题->01-并发编程之Executor线程池详解
01-并发编程之Executor线程池详解 线程:什么是线程&多线程 线程:线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系 ...
- nginx源码分析线程池详解
nginx源码分析线程池详解 一.前言 nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影响.但是经常会有人问道,n ...
随机推荐
- 20155209 2016-2017-2《Java程序设计》课程总结
20155209 2016-2017-2<Java程序设计>课程总结 预备作业1 刚刚接触Markdown的写法,刚刚接触博客,简单了解娄老师的教学方式. 预备作业2 怎么将学习java像 ...
- 20155211 2016-2017-2 《Java程序设计》第3周学习总结
20155211 2016-2017-2 <Java程序设计>第3周学习总结 教材学习内容总结 本周的内容还没有完全理解,将在上课前继续阅读. 教材学习中的问题和解决过程 在敲p88页的代 ...
- 分享daocloud联合创始人陈齐彦关于docker的一段阐述
罗比,本名陈齐彦,他在加入DaoCloud之前是EMC中国研究院的总架构师,云平台及应用实验室的创始人.谈及创业的初心,他激动了起来: 容器这东西和当年Hadoop一样,是互联网技术对企业IT技术的逆 ...
- 【MYSQL权限】数据库权限部署
背景:没有划分数据库权限,所有人共用一个账号 本人公司现有的数据库账号分布情况: 所有人用一个账号(包括程序里面访问数据库的的配置文件里面的账号),该账号除删库权限,其他权限大部分都有. 这样非数据库 ...
- Matplotlib API汉化
Pyplot API 示例汇总:https://matplotlib.org/gallery/index.html#api-examples 该matplotlib.pyplot模块包含的功能允许您快 ...
- Spring学习(十)-----Spring依赖检查
在Spring中,可以使用依赖检查功能,以确保所要求的属性可设置或者注入. 依赖检查模式 4个依赖检查支持的模式: none – 没有依赖检查,这是默认的模式. simple – 如果基本类型(int ...
- Http协议工作特点和工作原理笔记
工作特点: (1)B/S结构(Browser/Server,浏览器/服务器模式) (2)无状态 (3)简单快速.可使用超文本传输协议.灵活运行传输各种类型 工作原理: 客户端发送请求浏览器 -> ...
- 时序数据库InfluxDB
在系统服务部署过后,线上运行服务的稳定性是系统好坏的重要体现,监控系统状态至关重要,经过调研了解,时序数据库influxDB在此方面表现优异. influxDB介绍 时间序列数据是以时间字段为每行数据 ...
- Python:三元运算
result=值1 if 条件 else 值2 如果条件为真,result=值1 如果条件为假,result=值2 例子: a,b,c=1,3,5 d=a if a>b else c print ...
- lintcode-397-最长上升连续子序列
397-最长上升连续子序列 给定一个整数数组(下标从 0 到 n-1, n 表示整个数组的规模),请找出该数组中的最长上升连续子序列.(最长上升连续子序列可以定义为从右到左或从左到右的序列.) 注意事 ...