Tomcat与线程池

时间:2022-04-25
本文章向大家介绍Tomcat与线程池,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Tomcat是如何处理多个请求的呢,我们以排队买票为例子,说说三种方案:

1、火车站只提供一个窗口,所有的人都必须排队等待。大家都知道这是多么糟糕的体验,后来的人必须等前面的人买完票才能进入申请购票,更糟糕的是中间还会发生一些小意外,比如机器卡了,某个乘客因为一些小矛盾与售票员发生了激烈争执呀等等。从程序角度上说,就是server只用一个线程来处理所有请求任务,这不能充分使用服务器资源,是非常低效的一种策略,而前面的请求任务可能在连接数据库,读取文件时发生长时间阻塞,导致后来的请求进入长时间的等待状态。

2、火车站为每一个购票用户配备一个临时售票员,这刚开始是非常高效的,但随着购票用户的增加,整个火车站都将被挤爆。从程序角度说,就是每来一个请求,就创建一个线程处理,这样多个请求就可以被并行处理,大大提高的资源使用率和任务处理效率,但是创建线程本身就是消耗资源的,而大量空闲线程将占用了内存(超过上限后会报OutOfMemory异常),也使得cpu在频繁的上下文切换中造成了性能损耗。

3、火车站增加多个售票窗口,乘客仍然要排队,但处理效率更高了,哪个窗口闲了,就处理新的购票申请。这类似于tomcat中的线程池,线程池是用来管理工作线程的,一般和队列配合使用,他对线程进行重复使用,减少了频繁创建线程的消耗,同时可以对线程数量进行控制,在不超过负载的前提下,充分使用内存和cpu资源。

Tomcat创建线程池的方法在AbstractEndpoint类中,它有三个子类,分别用来实现tomcat connector 的三种运行模式:BIO,NIO和APR,在此我们仅针对BIO的运行模式进行分析。

该类有一个创建线程池的方法

public void createExecutor() {

internalExecutor = true;

TaskQueue taskqueue = new TaskQueue();

TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());

executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);

taskqueue.setParent( (ThreadPoolExecutor) executor);

}

说明一点,这个线程池主要是处理请求任务的,而对请求的接受主要由Acceptor(实现Runnable)完成,其线程数量由acceptorThreadCount指定,默认值是1。

我们再来看下ThreadPoolExecutor构造函数

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)

corePoolSize - 池中所保存的线程数,包括空闲线程。

maximumPoolSize - 池中允许的最大线程数。

keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

unit - keepAliveTime 参数的时间单位。

workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。

threadFactory - 执行程序创建新线程时使用的工厂。

一般规则是当运行线程少于corePoolSize,Executor将创建新线程处理任务,如果等于或多于corePoolSize,则请求将加入队列,而不创建新线程,如果无法加入队列,则创建新线程,直至大于maximumPoolSize ,任务被拒绝

以上规则结合ThreadPoolExecutor execute方法源码会更容易理解:

 int c = ctl.get();
 if (workerCountOf(c) < corePoolSize) {
  if (addWorker(command, true))
  return;
   c = ctl.get();
  }
/*
根据taskqueue的offer方法,若现有线程数量小于maxThreads,workQueue.offer(command)返回false,不放入队列,创建一个新线程处理请求任务(即addWorker(command,flase))
*/
 if (isRunning(c) && workQueue.offer(command)) {
  int recheck = ctl.get();
   if (!isRunning(recheck) && remove(command))
   reject(command);
  else if (workerCountOf(recheck) == 0)
   addWorker(null, false);
  } else if (!addWorker(command, false))
  reject(command);

maxThreads默认值是200,而TaskQueue对LinkedBlockingQueue的offer()方法进行了覆盖,添加了一些新的规则:

@Override
 public boolean offer(Runnable o) {
 // we can't do any checks
 if (parent == null)
 return super.offer(o);
 // we are maxed out on threads, simply queue the object
 if (parent.getPoolSize() == parent.getMaximumPoolSize())
 return super.offer(o);
 // we have idle threads, just add it to the queue
 if (parent.getSubmittedCount() < (parent.getPoolSize()))
 return super.offer(o);
 // if we have less threads than maximum force creation of a new thread
 if (parent.getPoolSize() < parent.getMaximumPoolSize())
 return false;
 // if we reached here, we need to add it to the queue
 return super.offer(o);
}

在加入队列过程中,若发现现有线程数小于最大线程数且没有空闲线程,它会创建新的线程。该队列默认是一个无界队列,现有线程数大于等于最大线程数时,请求任务会加入队列等待。

而且,tomcat创建线程线程数还受maxConnections限制,代码如下:

 // if we have reached max connections, wait
 countUpOrAwaitConnection();
  Socket socket = null;
 try {
 // Accept the next incoming connection from the server
 // socket
 socket = serverSocketFactory.acceptSocket(serverSocket);
  } catch (IOException ioe) {
 countDownConnection();
 // Introduce delay if necessary
 errorDelay = handleExceptionWithDelay(errorDelay);
 // re-throw
 throw ioe;
  }

当连接达到maxConnections时,请求不会被socket接受,而是进入TCP的完全连接队列中,队列的大小由acceptCount值决定,默认是100.

于是tomcat处理请求的过程便是:Acceptor接收一个请求,若现有线程数量小于maxThreads且没有空闲线程,则创建一个新线程处理请求任务,若超过maxThreads(BIO模式下,maxConnections默认值等同于maxThreads),则放入TCP完全连接队列中(注意,不是线程池中的队列),当队列大于acceptCount值时,则报“connection refused”错误。

虽然线程池技术提高了性能,缩短了请求响应时间,同时防止了突发性大量请求引起的资源耗尽,但其本质上还是一个线程处理一个请求,线程池技术结合NIO技术,让少量线程处理大量请求,将极大得提高并发能力,在tomcat6以后,已经实现了这一技术,只要将server.xml配置改成如下即可:

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"

connectionTimeout="20000" redirectPort="8443"/>

有关ThreadPoolExecutor的源码解读和Nio的内容,以后还会详细讲解。