线程的创建和销毁是十分耗性能的操作, 我们平常的工作在使用的线程也是生命周期还短暂的,如果不使用线程池就会频繁的创建销毁线程。 这很不划算,于是聪明的程序员们发明了线程池。
我们看下线程池的创建参数:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1, 10, 1, TimeUnit.HOURS,
new SynchronousQueue<Runnable>(),
new ThreadPoolExecutor.AbortPolicy()
);
看下线程池的几个参数的意义:
- 表示核心线程数,就是线程池中不会被回收的线程数
- 表示最大线程数, 就是线程池允许创建的最大线程数
- 第三个和第四个参数就是没有任务后的允许线程存活的时间
- 就是线程不够用后暂存任务的队列
- 最后如果队列不够用后的拒绝任务的策略
线程池的启动流程
- 创建线程池,设置各个参数, 此时没有一个线程
- execute或submit传入一个任务,同时会唤醒休眠的线程。
- 先判断线程数达到最大线程数没有, 如果没有达到就新建一个线程来运行任务
- 如果达到最大线程数,就开始会把任务加入队列中
- 任务队列也加满了,就会执行拒绝策略,
- 当某个线程运行完任务后, 会再次从队列中获取新的任务运行。
- 如果队列中没有任务,线程会休眠,休眠时间是传入的时间
- 某个线程休眠结束后,会再次从任务队列中获取任务,如果任务队列是空的, 则判断当前存活线程数是否大于核心线程数, 如果大于则这个线程就会死亡。
- 如果小于或者等于最小核心线程, 就会继续休眠。
任务的加入流程:
- 先判断线程数量小于核心线程, 就会调用addWorker方法
- 在addWorker 中会把任务包装成worker 。 worker中保存有线程和任务信息,内部的线程没有启动而已, 并把worker加入到HashSet中。
- 在addWorker中如果继续判断是否运行线程数到达最大线程,如果没有就启动最新创建的worker中的线程
- 如果在addWorker中启动线程成功就会任务加入就会完成。
- 如果在addWorker方法返回false, 有两种情况, 线程池状态不对,线程数量到达上限。 如果只是线程数量到达上限,就会把任务加入队列中。
- 如果状态不对,就会直接执行拒绝策略
定时任务线程池:
- 线程中任务的数量的上线没有限制
- 线程休眠等待时间10ms, 也就是定时任务的最小力度是10ms
- 实现了延时的DelayedWorkQueue来接收任务。 这个队列会根据对象的时间来排序任务的。
- 新建任务是先 ScheduledFuture对象, 再放入队列的。 之后在执行线程的
线程池中线程数的设置多少合理
程序中的任务我们分为IO密集型 和 运算密集型两种。
- 如果是运算密集型的任务, 我们吧cpu的核心数量作为线程数就可以。 防止阻塞等因素还可以在加上1
- IO密集型: 由于IO是不消耗cpu的,我们统计下任务中io耗时时间 和cpu耗时时间的比例, 如果比例是7:2 那么设置线程数为设置为 1+7/2 的值作为线程数比较合理,这样当IO运行完成后, cpu也运行完成了,不用互相等待浪费时间了。
- 我们常见的web应用等等, 几乎都是io密集型的, 主要io浪费在数据库链接和http请求等。 所以这个时候设置线程数多一点也没有关系
但是程序中IO耗时和cpu的耗时比例不好统计, 我们只能够在现实中多加观察和以往的经验来设置个线程数, 并多多观察jvm的监控日志来设置合理的线程数
Executors 线程池创建工具
Executors.newCachedThreadPool()
创建的线程池核心线程0 , 最大线程是Integer.MaxValue。 线程空闲存活时间1分钟。 默认异常拒绝策略,使用SynchronousQueue队列。它的特点:
- 每次添加任务如果没有空闲线程就会新建一个线程去执行。
- SynchronousQueue是阻塞队列,加入任务的线程会阻塞住,直到其它线程从中取走任务才会结束阻塞
- 他的线程创建上限近乎无限
所以它适用于任务加入比较稳当且加入间隔短的场景
Executors.newSingleThreadExecutor()
核心和最大线程数都是1, 空闲存活时间为0 , 任务队列是无线长度的LinkedBlockingQueue。 默认异常拒绝策略。它的特点:
- 只有一个线程
- 近乎可以接收无限任务的队列, 可以堆积大量任务
适用于任务持续加入但是任务数并不多的场景
Executors.newFixedThreadPool(3)
核心线程和最大线程数是你传入的参数。 其他参数和 Executors.newSingleThreadExecutor一样
Executors.newScheduledThreadPool(1)
这个是用于定时任务的线程池, 内部实现和上面三个都有不同。
- 核心线程是传入的参数,最大线程是int上线, 默认存活时间是10毫秒, 任务队列使用自己实现的DelayedWorkQueue, 拒绝策略异常策略
- 加入任务的时候,会把任务和定时时间构建一个RunnableScheduledFuture对象,再把这个对象放入DelayedWorkQueue队列中,
- DelayedWorkQueue是一个有序队列, 他会根据内部的RunnableScheduledFuture的运行时间排序内部对象。
- 任务加入后就会启动一个线程。 这个线程会从DelayedWorkQueue中获取一个任务。
- DelayedWorkQueue内部是按照时间从前完后获取任务的。如果任务的中的时间还没有到。 获取的就是null。 获取任务结束,线程会休眠10毫秒。所以这个定时任务的执行最小间隔是10毫秒的。
由于Executors创建的线程池还多参数的设置都是什么Integer.MaxValue和无上限的BlockQuene等, 如果我们不加思考的使用,很容易把线程池用在错误的场景上, 因此阿里巴巴的编程手册上不推荐Executors创建线程池的方式, 它要求我们必须自己设置参数来创建线程池。
阿里的java手册中要求, 线程池必须自己设置参数来创建,严禁使用Executors来创建线程池。 这个要求是有道理的, 我们来看下为何不推荐
不推荐使用Executors来创建线程池
我们逐个看下executors创建的各个线程池的参数
Executors.newCachedThreadPool()
最大线程数是Integer.MAX_VALUE, 并且任务队列是SynchronousQueue
。 也就是说这个线程池对任务来着不拒,线程不够用就创建一个, 感觉就像一个豪横的富豪。 这就是问题所在了, 如果同一时刻应用的来了大量的任务, 这个线程池很容易就创建过多的线程, 而创建线程又是一个很耗性能的事情, 这就容易导致应用卡顿或者直接OOM
Executors.newFixedThreadPool(1)
这个线程池到时没有上个线程池豪横了, 它定死了线程数量, 所以线程数量是不会超出的,但是它的任务队列是无界的LinkedBlockingQueue
, 对于加进来的任务处理不过来就会存入任务队列中, 并且无限制的存入队列。 这个线程池感觉就是家里有地, 无论来多少货都往里面装。
这个线程池如果使用不当很容易导致OOM
Executors.newSingleThreadExecutor()
这个线程池只有一个线程, 比newFixedThreadPool还穷, 但是任务队列和上面一样, 没有限制, 很容易就使用不当导致OOM
Executors.newScheduledThreadPool(2)
这个是定时任务的线程池, 没有定义线程创建数量的上线, 同时任务队列也没有定义上限, 如果前一次定时任务还没有完成, 后一个定时任务的运行时间到了, 它也会运行, 线程不够就创建。 这样如果定时任务运行的时间过长, 就会导致前后两个定时任务同时执行,如果他们之间有锁,还有可能出现死锁, 此时灾难就发生了。
举个例子:
- 某条公交路线平时只有两辆公交车在跑, 只有乘客太多的早晚高峰才会增加班车。
- 但是某一天 第一班车因为某种原因开的慢, 被第二班车追上了,并且两车发生事故堵在路上了,并且把路堵死了,
- 但是这个时候公交调度站不知道情况, 它只要看到站台有乘客等车就会增加一班车,
- 新增的班车来到事故点也被堵住了, 无法进行下去了
- 但是调度站只会根据乘客情况无限制发车,最终导致整个公交线路瘫痪。
所以使用这个线程池有一定的风险, 建议使用spring的定时任务模块, 他可以设置成第一个定时任务没有完成, 第二定时任务不触发。
总结
线程池是一个很好的重用资源的方式, 类似还有数据库连接池等, 但是业务场景是各种各样的,我们要根据不同的业务设置不同的线程池参数, 而不要使用Executors进行偷懒, Executors的很多的参数设置并不合理。
线程池使用还有一些注意问题和陷阱, 这些我会留在下篇博客中说明的。