Java 线程池详解
构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?Runnable和Callable的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片段。
基础知识
Executors创建线程池
Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。
Executors创建线程池便捷方法列表:
小程序使用这些快捷方法没什么问题,对于服务端需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor的构造方法。没错,上述Executors方法创建的线程池就是ThreadPoolExecutor。
ThreadPoolExecutor构造方法
Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:
// 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的一种补充,允许有返回值,允许抛出异常。
三种提交任务的方式:
如何正确使用线程池
避免使用无界队列
不要使用Executors.newXXXThreadPool()
快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:
ExecutorService executorService = new ThreadPoolExecutor(2, 2,0, TimeUnit.SECONDS,new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOMnew ThreadPoolExecutor.DiscardPolicy());
明确拒绝任务时的行为
任务队列总有占满的时候,这是再submit()
提交新的任务会怎么样呢?RejectedExecutionHandler
接口为我们提供了控制方式,接口定义如下:
public interface RejectedExecutionHandler {void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
线程池给我们提供了几种常见的拒绝策略:
线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。
ExecutorService executorService = new ThreadPoolExecutor(2, 2,0, TimeUnit.SECONDS,new ArrayBlockingQueue<>(512),new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略
获取处理结果和异常
线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionException,submit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:
ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {@Overridepublic 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();
}
上述代码输出类似如下:
线程池的常用场景
正确构造线程池
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()方法,就能获取所有任务的执行结果,获取顺序是任意的,取决于任务的完成顺序:
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完成:
public void testLatch(ExecutorService executorService, List<Runnable> tasks)throws InterruptedException{CountDownLatch latch = new CountDownLatch(tasks.size());for(Runnable r : tasks){executorService.submit(new Runnable() {@Overridepublic void run() {try{r.run();}finally {latch.countDown();// countDown}}});}latch.await(10, TimeUnit.SECONDS); // 指定超时时间}
线程池和装修公司
以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。
线程池就是程序中的“装修公司”,代劳各种脏活累活。上面的过程对应到线程池上:
// Java线程池的完整构造函数
public ThreadPoolExecutor(int corePoolSize, // 正式工数量int maximumPoolSize, // 工人数量上限,包括正式工和临时工long keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇BlockingQueue<Runnable> workQueue, // 排期队列ThreadFactory threadFactory, // 招人渠道RejectedExecutionHandler handler) // 拒单方式
总结
Executors为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch完成。
参考
ThreadPoolExecutor API Doc
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html
Java 线程池详解相关推荐
- Java线程池详解学习:ThreadPoolExecutor
Java线程池详解学习:ThreadPoolExecutor Java的源码下载参考这篇文章:Java源码下载和阅读(JDK1.8) - zhangpeterx的博客 在源码的目录java/util/ ...
- Java 线程池详解学习:FixedThreadPool,CachedThreadPool,ScheduledThreadPool...
Java常用的线程池有FixedThreadPool和CachedThreadPool,我们可以通过查看他们的源码来进行学习. Java的源码下载参考这篇文章:Java源码下载和阅读(JDK1.8) ...
- Java 线程池详解及实例代码
转载自 Java 线程池详解及实例代码 这篇文章主要介绍了Java 线程池的相关资料,并符实例代码,帮助大家学习参考,需要的朋友可以参考下 线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时 ...
- java线程池详解及五种线程池方法详解
基础知识 Executors创建线程池 Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThrea ...
- 【转载】Java线程池详解
目录 1.线程池的优势 2.Java中的ThreadPoolExecutor类 3.线程池的主要参数 4.线程池流程 5.线程池为什么需要使用(阻塞)队列? 6.线程池为什么要使用阻塞队列而不使用非阻 ...
- 【java线程系列】java线程系列之java线程池详解
一线程池的概念及为何需要线程池: 我们知道当我们自己创建一个线程时如果该线程执行完任务后就进入死亡状态,这样如果我们需要在次使用一个线程时得重新创建一个线程,但是线程的创建是要付出一定的代价的,如果在 ...
- 一文详解java线程池 详解Java线程池的七个参数 详解池化技术 java如何选择核心线程数 详解Java线程池的拒绝策略
目录 引言 线程池使用场景 加快请求响应(响应时间优先) 加快处理大任务(吞吐量优先) 特殊说明 线程池的池化技术 线程池的创建 手动创建 创建newFixedThreadPool线程池 创建newS ...
- Java线程池详解及常用方法
前言 最近被问到了线程池的相关问题.于是准备开始写一些多线程相关的文章.这篇将介绍一下线程池的基本使用. Executors Executors是concurrent包下的一个类,为我们提供了创建线程 ...
- Java多线程之线程池详解
Java多线程之线程池详解 目录: 线程池使用及优势 线程池3个常用方式 线程池7大参数深入介绍 线程池底层工作原理 1. 线程池使用及优势 线程池做的工作主要是控制运行的线程的数量,处理过程中将任务 ...
最新文章
- Do a test write by windows live writer
- QML资源加载和网络透明度
- 【蓝桥杯Java_C组·从零开始卷】第二节、ifswitch(超级详细,适合入门强化学习,理论与实战双保险(2021年12月30日上午9点直播·视频发布B站)
- iis mysql版本切换_MySQL+PHP配置 Windows系统IIS版(转)
- jquer each 遍历的结果不显示 null_SpringBoot系列(三十一)- Thymeleaf如何用th:each 做条件遍历
- linux sed名宁,Linux shell利用sed批量更改文件名的方法
- ORA-65096: 公用用户名或角色名无效引发的思考
- php psl标准,psl是什么单位
- JavaFX中将FXML文件自动转换为Java代码
- gwas snp 和_eQTL和GWAS还可以这样玩
- php 根据一个数据组的id 取得另一个数据组对应的名称,怎样从两个spss的数据文件中选出id相同的数据,合并成一个文件...
- 用python自动办公 麻瓜_(视频教程)下载:麻瓜编程Python商业爬虫学徒计划python自动化办公麻瓜麻瓜编程...
- 此计算机不支持动态磁盘,磁盘无法分区提示此操作系统不支持动态磁盘故障原因分析与解决...
- 万字攻略全面了解selenium_selenium教程
- 小型元器件介绍:排阻
- 基于Arduino的PS2机械臂遥控小车(L298N)傻瓜教程
- Qt小游戏教程之贪吃蛇(带源码)
- 常见计算机主机内部硬件设备,计算机的硬件主要包括中央处理器、储存器、输出设备和...
- 《雷军:我向阿里学到了三点,这是创业成功的核心》有感
- 每周一学表格WPS函数公式:将文本日期转换成正常日期格式