Java ThreadPool的正确打开方式
线程池应对于突然增大、来不及处理的请求,无非两种应对方式:
- 将未完成的请求放在队列里等待
- 临时增加处理线程,等高峰回落后再结束临时线程
JDK的Executors.newFixedPool() 和newCachedPool(),分别使用了这两种方式。
不过,这俩函数在方便之余,也屏蔽了ThreadPool原本多样的配置,对一些不求甚解的码农来说,就错过了一些更适合自己项目的选择。
1. ThreadPoolExecutor的原理
经典书《Java Concurrency in Pratice(Java并发编程实战)》第8章,浓缩如下:
1. 每次提交任务时,如果线程数还没达到coreSize就创建新线程并绑定该任务。
所以第coreSize次提交任务后线程总数必达到coreSize,不会重用之前的空闲线程。
2. 线程数达到coreSize后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用take()从工作队列里拉活来干。
3. 如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。
4. 临时线程使用poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。
5. 如果core线程数+临时线程数 >maxSize,则不能再创建新的临时线程了,转头执行RejectExecutionHanlder。默认的AbortPolicy抛 RejectedExecutionException异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务 (DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。
2. FixedPool 与 CachedPool
FixedPool默认用了一条无界的工作队列 LinkedBlockingQueue, 所以只去到上面的第2步就不会继续往下走了,coreSize的线程做不完的任务不断堆积到无限长的Queue中,所以只有coreSize一个参数,其他maxSize,keepAliveTime,RejectHandler的配置都不会实际生效
CachedPool则把coreSize设成0,然后选用了一种特殊的Queue --SynchronousQueue,只要当前没有空闲线程,Queue就会立刻报插入失败,让线程池增加新的临时线程,默认 KeepAliveTime是1分钟,而且maxSize是整形的最大值,也就是说只要有干不完的活,都会无限增增加线程数,直到高峰过去线程数才会回落。
3. 对FixedPool的进一步配置
3.1 设置QueueSize
如果不想搞一条无限长的Queue,避免任务无限等待显得像假死,同时占用太多内存,可能会把它换成一条有界的ArrayBlockingQueue,那就要同时关注一下这条队列满了之后的场景,选择正确的rejectHanlder。
此时,最好还是把maxSize设为coreSize一样的值,不把临时线程及其keepAlive时间拉进来,Queue+临时线程两者结合听是好听,但很难设置好。
4. 对CachedPool的进一步配置
4.1 设置coreSize
coreSize默认为0,但很多时候也希望是一个类似FixedPool的固定值,能处理大部分的情况,不要有太多加加减减的波动,等待和消耗的精力。
4.2 设置maxSize及rejectHandler
同理,maxSize默认是整形最大值,但太多的线程也很可能让系统崩溃,所以建议还是设一下maxSize和rejectHandler。
4.3 设置keepAliveTime
默认1分钟,可以根据项目再设置一把。
5. SpringSide的ThreadPoolBuilder
广告时间,SpringSide的ThreadPoolBuilder能简化上述的配置。
此文太科普,不是为了帮SpringSide里的Utils打广告也不会写