线程池的前世今生
线程池的前世今生
多线程处理的技术已经在各种应用系统中被经常使用.但是如果我们使用不当,往往会引发一些莫名其妙的问题.
而多线程使用最多的,是JDK中提供的ThreadPoolExecutor,按照阿里巴巴的java规范,建议不要使用Executes来创建示例,而是直接使用构造函数.ThreadPoolExecutor一共有7个构造函数.
corePoolSize
线程池中核心线程的数量。当提交一个任务时,线程池会新建一个线程来执行任务,直到当前线程数等于corePoolSize。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize
线程池中允许的最大线程数。线程池的阻塞队列满了之后,如果还有任务提交,如果当前的线程数小于maximumPoolSize,则会新建线程来执行任务。注意,如果使用的是无界队列,该参数也就没有什么效果了。
keepAliveTime
线程空闲的时间。线程的创建和销毁是需要代价的。线程执行完任务后不会立即销毁,而是继续存活一段时间:keepAliveTime。默认情况下,该参数只有在线程数大于corePoolSize时才会生效。
TimeUnit
keepAliveTime的单位。
workQueue
用来保存等待执行的任务的阻塞队列,等待的任务必须实现Runnable接口.Queue的深度不在当前的构造函数内,而是通过传入Queue对象,间接实现Queue的深度定制.
ThreadFactory
用于设置创建线程的工厂。该对象可以通过Executors.defaultThreadFactory(),一般是用这个参数来自定义线程的执行优先级,线程名称等.
RejectedExecutionHandler
线程池的拒绝策略。所谓拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略。当向线程池中提交任务时,如果此时线程池中的线程已经饱和了,而且阻塞队列也已经满了,则线程池会选择一种拒绝策略来处理该任务
拒绝策略一共有以下几种
| 策略 | 说明 |
|---|---|
| AbortPolicy | 会在调用ThreadPoolExecutor.execute()的时候,抛出一个异常 |
| DiscardPolicy | 直接忽略当前提交的内容,啥都不干 |
| DiscardOldestPolicy | 移除队列中的第一个元素,把当前元素插入到队列末尾 |
| CallerRunsPolicy | 默认值,使用当前ThreadPoolExecutor.execute()的线程(相当于使用第maxSize+1的线程)去处理当前的任务 |
运行过程
JDK介绍完了,我们需要了解一下线程池的运行过程.
假设我们使用3个构造函数的ThreadPoolExecutor(coresize , maxSize , keep , timeUnit , Queue)来构造一个threadPool,假设Runnable的执行,是一个sleep,一直不释放,每秒往线程池放一个任务.
| 参数 | 数值 |
|---|---|
| coreSize | 10 |
| maxSize | 20 |
| keep | 100000 |
| timeUnit | Hour |
| Queue | new LinkQueue(100) |
刚开始,活动线程=0,向线程池提交任务的时候,线程池会创建一个线程,来执行这个任务.活动线程+1.
继续往线程池提交任务,当活动线程>=coreSize的时候,这个任务会被加入Queue.
继续往线程池提交任务,因为活动线程一直没有释放,Queue会在110秒的时候被塞满.
继续往线程池提交任务,这个时候活动线程=coreSize,而Queue已经塞满,提交的任务会新创建一个线程来执行,同时活动线程+1.
继续往线程池提交任务,在120秒的时候,活动线程=maxSize,Queue已经塞满.
继续往线程池提交任务,这个任务将触发RejectedExecutionHandler,这个任务将会使用submit()的线程来执行,执行到内部的sleep的时候,线程等待,submit()的方法将不会有返回,因为当前线程已经sleep.
上述过程就是整个ThreadPool的执行过程.
所以,回答一个问题,线程池的执行线程设置为1的时候,能不能保证所有任务被顺序执行??
常见的问题
示例
线程池设置不好,经常出问题,都有相似的特征.
我们经常使用netty来做网络通讯,netty中的BOSS线程在接收到一个网络请求,交给worker线程执行,这个交接就是通过ThreadPoolExecutor进行的.我们经常出问题的现象是这样的.
现在,假设这个netty的通讯有2种类型的处理,一种处理耗时比较低(2ms),但是请求量大,一种请求耗时比较长(2min),但是请求数量较少,假设这2种请求的数量比为10:1
有可能出现的现象
- 延迟低的请求也会因为线程池阻塞,造成原本处理2ms就返回的请求,会延迟
- boss线程被阻塞,造成端口监听出现问题,无论发送什么请求都没有响应
原理分析
第一次提交10个请求的情形,10个请求很快被处理完,1个请求占用一个线程慢慢处理.

后面再次提交请求的时候,如果有处理慢的请求都会长时间占用线程

直到最后的被堵塞的情形

出现的问题的情形和线程池的设置和任务处理的时长有关.
解决思路
碰上这种间歇性处理时长的问题,一般都是线程池的问题.
有个简单的临时解决方案,就是分流,所有的请求都在一个线程池内部处理变成快的请求在一个线程池,慢的请求在另外一个线程池,慢的请求的线程池的deep可以设置的深一些,拒绝策略不能再用CallRunner,而应该使用DiscardOldestPolicy.
上述的场景在普通的RPC框架是也是存在的,比如dubbo.虽然在netty的worker又增加了个线程池,但是无法判断一个请求的处理时间,没有做快慢分离,一旦出现比较慢的请求阻塞,就容易歇菜.
springCloud考虑的就更少了,但是因为复用HTTP通道,通常HTTP通道的处理线程会设置的比较大,经常在1000以上,处理延迟比较大,一般在30s左右,很少出现线程池的执行线程不够的问题.
写在最后
线程池只是我们平常处理的业务的手段,线程池用的好不好,直接决定了我们业务处理是否顺畅.最终解决线程池问题的,其实是解决业务处理中处理慢的问题.
