如何更好的使用JAVA线程池

Doug Lea
ExecutorService基于池化的线程来执行用户提交的任务,通常可以简单的通过Executors提供的工厂方法来创建ThreadPoolExecutor实例。
线程池解决的两个问题:
- 线程池通过减少每次做任务的时候产生的性能消耗来优化执行大量的异步任务的时候的系统性能。
- 线程池还提供了限制和管理批量任务被执行的时候消耗的资源、线程的方法。另外ThreadPoolExecutor还提供了简单的统计功能,比如当前有多少任务被执行完了。
快速开始
为了使得线程池适合大量不同的应用上下文环境,ThreadPoolExecutor提供了很多可以配置的参数和可被用来扩展的钩子。然而,用户还可以通过使用Executors提供的一些工厂方法来快速创建ThreadPoolExecutor实例。比如:
- 使用Executors#newCachedThreadPool可以快速创建一个拥有自动回收线程功能且没有限制的线程池。
- 使用Executors#newFixedThreadPool可以用来创建一个固定线程大小的线程池。
- 使用Executors#newSingleThreadExecutor可以用来创建一个单线程的执行器。
如果上面的方法创建的实例不能满足我们的需求,我们可以自己通过参数来配置,实例化一个实例。
关于线程数大小参数设置需要知道的
ThreadPoolExecutor会根据corePoolSize和maximumPoolSize来动态调整线程池的大小:poolSize。
当任务通过executor提交给线程池的时候,我们需要知道下面几个点:
- 如果这个时候当前池子中的工作线程数小于corePoolSize,则新创建一个新的工作线程来执行这个任务,不管工作线程集合中有没有线程是处于空闲状态。
- 如果池子中有比corePoolSize大的但是比maximumPoolSize小的工作线程,任务会首先被尝试着放入队列,这里有两种情况需要单独说一下:
- 如果任务被成功的放入队列,则看看是否需要开启新的线程来执行任务,只有当当前工作线程数为0的时候才会创建新的线程,因为之前的线程有可能因为都处于空闲状态或因为工作结束而被移除。
- 如果放入队列失败,则才会去创建新的工作线程。
- 如果corePoolSize和maximumPoolSize相同,则线程池的大小是固定的。
- 通过将maximumPoolSize设置为无限大,我们可以得到一个无上限的线程池。
- 除了通过构造参数设置这几个线程池参数之外我们还可以在运行时设置。
核心线程WarmUp
默认情况下,核心工作线程值在初始的时候被创建,当新任务来到的时候被启动,但是我们可以通过重写prestartCoreThread或prestartCoreThreads方法来改变这种行为。通常场景我们可以在应用启动的时候来WarmUp核心线程,从而达到任务过来能够立马执行的结果,使得初始任务处理的时间得到一定优化。
定制工作线程的创建
新的线程是通过ThreadFactory来创建的,如果没有指定,默认的Executors#defaultThreadFactory将被使用,这个时候创建的线程将都属于同一个线程组,拥有同样的优先级和daemon状态。扩展配置ThreadFactory,我们可以配置线程的名字、线程组合daemon状态。如果调用ThreadFactory#createThread的时候失败,将返回null,executor将不会执行任何任务。
空闲线程回收
如果当前池子中的工作线程数大于corePoolSize,如果超过这个数字的线程处于空闲的时间大于keepAliveTime,则这些线程将会被终止,这是一种减少不必要资源消耗的策略。这个参数可以在运行时被改变,我们同样可以将这种策略应用给核心线程,我们可以通过调用allowCoreThreadTimeout来实现。
选择合适的阻塞队列
所有的阻塞队列都可以被用来存放任务,但是使用不同的队列针对corePoolSize会表现不同的行为:
当池中工作线程数小于corePoolSize的时候,每次来任务的时候都会创建一个新的工作线程。
当池中工作线程数大于等于corePoolSize的时候,每次任务来的时候都会首先尝试将线程放入队列,而不是直接去创建线程。
如果放入队列失败,且当先池中线程数小于maximumPoolSize的时候,则会创建一个工作线程。
下面主要是不同队列策略表现:
直接递交:一种比较好的默认选择是使用SynchronousQueue,这种策略会将提交的任务直接传送给工作线程,而不持有。如果当前没有工作线程来处理,即任务放入队列失败,则根据线程池的实现,会引发新的工作线程创建,因此新提交的任务会被处理。这种策略在当提交的一批任务之间有依赖关系的时候避免了锁竞争消耗。值得一提的是,这种策略最好是配合unbounded线程数来使用,从而避免任务被拒绝。同时我们必须要考虑到一种场景,当任务到来的速度大于任务处理的速度,将会引起无限制的线程数不断的增加。
无界队列:使用无界队列如LinkedBlockingQueue没有指定最大容量的时候,将会引起当核心线程都在忙的时候,新的任务被放在队列上,因此,永远不会有大于corePoolSize的线程被创建,因此maximumPoolSize参数将失效。这种策略比较适合所有的任务都不相互依赖,独立执行。举个例子,如网页服务器中,每个线程独立处理请求。但是当任务处理速度小于任务进入速度的时候会引起队列的无限膨胀。
有界队列:有界队列如ArrayBlockingQueue帮助限制资源的消耗,但是不容易控制。队列长度和maximumPoolSize这两个值会相互影响,使用大的队列和小maximumPoolSize会减少CPU的使用、操作系统资源、上下文切换的消耗,但是会降低吞吐量,如果任务被频繁的阻塞如IO线程,系统其实可以调度更多的线程。使用小的队列通常需要大maximumPoolSize,从而使得CPU更忙一些,但是又会增加降低吞吐量的线程调度的消耗。总结一下是IO密集型可以考虑多些线程来平衡CPU的使用,CPU密集型可以考虑少些线程减少线程调度的消耗。
选择适合的拒绝策略
当新的任务到来的而线程池被关闭的时候,或线程数和队列已经达到上限的时候,我们需要去做一个决定,怎么拒绝这些任务。下面介绍一下常用的策略:
ThreadPoolExecutor#AbortPolicy:这个策略直接抛出RejectedExecutionException异常。
ThreadPoolExecutor#CallerRunsPolicy:这个策略将会使用Caller线程来执行这个任务,这是一种feedback策略,可以降低任务提交的速度。
ThreadPoolExecutor#DiscardPolicy:这个策略将会直接丢弃任务。
ThreadPoolExecutor#DiscardOldestPolicy:这个策略将会把任务队列头部的任务丢弃,然后重新尝试执行,如果还是失败则继续实施策略。
除了上面的几种策略,我们也可以通过实现RejectedExecutionHandler来实现自己的策略。
利用Hook嵌入你的行为
ThreadPoolExecutor提供了protected类型可以被覆盖的钩子方法,允许用户在任务执行之前会执行之后做一些事情。我们可以通过它来实现比如初始化ThreadLocal、收集统计信息、如记录日志等操作。这类Hook如beforeExecute和afterExecute。另外还有一个Hook可以用来在任务被执行完的时候让用户插入逻辑,如rerminated。
如果hook方法执行失败,则内部的工作线程的执行将会失败或被中断。
可访问的队列
getQueue方法可以用来访问queue队列以进行一些统计或者debug工作,我们不建议用作其他用途。同时remove方法和purge方法可以用来将任务从队列中移除。
关闭线程池
当线程池不在被引用并且工作线程数为0的时候,线程池将被终止。我们也可以调用shutdown来手动终止线程池。如果我们忘记调用shutdown,为了让线程资源被释放,我们还可以使用keepAliveTime和allowCoreThreadTimeOut来达到目的。
写在最后
JAVA本身提供的API已经可以让我们快速的进行基于线程池的多线程开发,但是我们必须要为我们写的代码负责,每一个参数的设置和策略的选择跟不同应用场景有绝对的关系。然而对于不同参数和不同策略的选择并不是一件容易的事情,我们必须要先回答一些基础问题:每创建一个线程,操作系统为我们做了哪些事情,这个线程的操作系统资源消耗主要在哪部分?假如我的应用场景是IO密集型的,那么我需要更多的线程还是更少的线程?假如我们的CPU操作和IO操作大概各占一半的话我们又需要如何选择?等等一些列问题。我认为、多线程开发是一件很容易的事情也是一件很不容易的事情。:)
参考文档《JDK1.5》 by Dong Lea
如何更好的使用JAVA线程池的更多相关文章
- 更好的使用JAVA线程池
这篇文章分别从线程池大小参数的设置.工作线程的创建.空闲线程的回收.阻塞队列的使用.任务拒绝策略.线程池Hook等方面来了解线程池的使用,其中涉及到一些细节包括不同参数.不同队列.不同拒绝策略的选择. ...
- Java线程池实现原理及其在美团业务中的实践
本文转载自Java线程池实现原理及其在美团业务中的实践 导语 随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流.使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器.J.U.C提供 ...
- Java线程池实现原理及其在美团业务中的实践(转)
转自美团技术团队:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html 随着计算机行业的飞速发展,摩尔定律逐 ...
- 四种Java线程池用法解析
本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 http://www.jb51.net/article/81843.htm 1.new Thread的弊端 执行一个异步任务你还只是如下 ...
- Java线程池的几种实现 及 常见问题讲解
工作中,经常会涉及到线程.比如有些任务,经常会交与线程去异步执行.抑或服务端程序为每个请求单独建立一个线程处理任务.线程之外的,比如我们用的数据库连接.这些创建销毁或者打开关闭的操作,非常影响系统性能 ...
- Java线程池应用
Executors工具类用于创建Java线程池和定时器. newFixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程.在任意点,在大多数 nThread ...
- Java线程池的原理及几类线程池的介绍
刚刚研究了一下线程池,如果有不足之处,请大家不吝赐教,大家共同学习.共同交流. 在什么情况下使用线程池? 单个任务处理的时间比较短 将需处理的任务的数量大 使用线程池的好处: 减少在创建和销毁线程上所 ...
- Java线程池与java.util.concurrent
Java(Android)线程池 介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端执行 ...
- Java线程池学习
Java线程池学习 Executor框架简介 在Java 5之后,并发编程引入了一堆新的启动.调度和管理线程的API.Executor框架便是Java 5中引入的,其内部使用了线程池机制,它在java ...
随机推荐
- 在linux环境下安装redis并且搭建自己的redis集群
此文档主要介绍在linux环境下安装redis并且搭建自己的redis集群 搭建环境: ubuntun 16.04 + redis-3.0.6 本文章分为三个部分:redis安装.搭建redis集群 ...
- 20155207 2006-2007-2 《Java程序设计》第4周学习总结
20155207 2006-2007-2 <Java程序设计>第4周学习总结 教材学习内容总结 ISP原则:一个类对另一个类的依赖应该限制在最小化的接口上. OCP原则:软件构成(类,模块 ...
- XMAPP 的安装与配置
1.XMAPP简介 1.1.XAMPP(Apache+MySQL/MariaDB+PHP+Perl) 开头的X代表X-OS,代表可以在任何常见操作系统下使用,包括Windows.Mac.Linux ...
- 20155321 2016-2017-2《Java程序设计》课堂实践项目2
20155321 2016-2017-2<Java程序设计>课堂实践项目2 实践内容 编写MyCP.java 实现类似Linux下cp XXX1 XXX2的功能,要求MyCP支持两个参数: ...
- 百度地图Map属性和方法
map的L属性:TANGRAM__1 map的F属性:[object Object] map的xa属性:[object HTMLDivElement] map的width属性:1340 map的hei ...
- js日期控件遇到的问题
一.问题: 在web项目里有很多时候需要使用日期控件来完成相关的功能,但是日期控件的日期格式又和我们的需求不符 那么,就需要我们来自定义日期的格式完成需求 二.解决: 1.取月末: (1)强制取值: ...
- sql语句-8-sql学习流程
- 4040 EZ系列之奖金 (拓扑)
4040 EZ系列之奖金 时间限制: 1 s 空间限制: 64000 KB 题目等级 : 钻石 Diamond 题目描述 Description 由于无敌的WRN在2015年世界英俊帅 ...
- Redis可视化客户端管理Web UI工具收集
https://github.com/uglide/RedisDesktopManager(推荐,全平台支持的桌面UI工具) Web方案: https://github.com/ErikDubbelb ...
- sql server 批量备份数据库
很多时候,我们都需要将数据库进行备份,当服务器上数据库较多时,不可能一个数据库创建一个定时任务进行备份,这时,就需要进行批量的数据库备份操作,好了,废话不多说,具体实现语句如下: --开启文件夹权限 ...