Java ThreadPool的正确打开方式

jopen 9年前

线程池应对于突然增大、来不及处理的请求,无非两种应对方式:

  1. 将未完成的请求放在队列里等待
  2. 临时增加处理线程,等高峰回落后再结束临时线程

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打广告也不会写 :)

来自:http://calvin1978.blogcn.com/articles/java-threadpool.html