说起线程池,大家可能受连接池的印象影响,天然的认为,它应该是一开始有core条线程,忙不过来了就扩展到max条线程,闲的时候又回落到core条线程,如果还有更高的高峰,就放进一个缓冲队列里缓冲一下。
有些整天只和SSH打交道的同学,可能现在还是这样认为的。
无情的现实就是,JDK只有两种典型的线程池,FixPool 与 CachedPool:
在 Java ThreadPool的正确打开方式 里,建议了如何设置,避免上面两句吓人的“无限”。但无论怎么配,都无法用现成的东西,配出文章一开始的想象图来。
但不得不说,这幅想象图还是比较美好的,特别对于偶有卡顿,偶有不可预测高峰的业务线程池来说。
当然,也有人说请求积压在最后的缓冲队列里不好控制,看具体业务场景了,缓冲队列也有不同的玩法(后详)。
我们的同事老王,研究了一番ThreadPool的机制后,提出了自己实现队列的方式。碰巧,Tomcat也正是这么做的,比起Jetty完全自己写线程池,Tomcat基于JDK的线程池稍作定制,要斯文一些。
JDK线程池的逻辑很简单( 更详细描述还是见 Java ThreadPool的正确打开方式 )
- 前core个请求,来一个请求就创建一个线程。
- 之后,把请求插入缓冲队列里让所有的线程去抢;如果插入失败则创建新线程。
- 如果达到max条线程了,抛出拒绝异常。
貌似控制的枢纽都在第2句那里--队列插入的结果。JDK也是通过使用LinkedBlockingQueue 与 特殊的SynchronousQueue,实现自己的控制效果。
那我可不可以自己封装一个Queue,在插入时增加以下逻辑呢?
说白了就是这么简单。
Tomcat的 TaskQueue 实现:
public class TaskQueue extends LinkedBlockingQueue { @Override public boolean offer(Runnable o) { // if (parent . getPoolSize() == parent . getMaximumPoolSize()) return super . offer(o);
// if (parent . getSubmittedCount() < parent.getPoolSize()) return super . offer(o); // if (parent . getPoolSize() < parent . getMaximumPoolSize()) return false; // return super.offer(o); } }
非常简单的代码,唯一要说明的是,如何判断当前有没有空闲的线程等待接客。JDK的CachedPool是靠特殊的零长度的SynchronousQueue实现。而Tomcat则靠 扩展Executor ,增加一个当前请求数的计数器,在execute()方法前加1,再重载afterExecute()方法减1,然后判断当前线程总数是否大于当前请求总数就知道有咩有围观群众。
因为相信Tomcat这种百年老店,我们就不自己写这个池了,把Tomcat实现里一些无关需求剥掉即用。
但Tomcat就完美了吗?
首先,TaskQueue的offer()里,调用了executor.getPoolSize(),这是个有锁的函数,这是最遗憾的地方。而且,最过分的是,Tomcat居然一口气调了三次。反复看了下,不求那么精准的话貌似一次就够了,真的有并发的变化的情况,executor里还有个处理RejectException,把任务重新放回队列的保险。
最后,说说缓冲队列的两种玩法:
一种是队列相对比较长,比如4096,主线程把任务丢进去就立刻返回了,如果队列满了就直接报拒绝异常。
一种是队列相对比较短的,比如512,如果满了,主线程就以queue.force(command, timeout)等在那里等队列有空,等到超时才报拒绝异常。
Tomcat的机制支持这两种玩法,自己设置就好。
文章可能还要修改,转载请保留原文链接,否则视为侵权:
http://calvin1978.blogcn.com/articles/tomcat-threadpool.html一只Tomcat: