咱们以排队买票为例子,说说三种方案:java
一、火车站只提供一个窗口,全部的人都必须排队等待。你们都知道这是多么糟糕的体验,后来的人必须等前面的人买完票才能进入申请购票,更糟糕的是中间还会发生一些小意外,好比机器卡了,某个乘客由于一些小矛盾与售票员发生了激烈争执呀等等。从程序角度上说,就是server只用一个线程来处理全部请求任务,这不能充分使用服务器资源,是很是低效的一种策略,而前面的请求任务可能在链接数据库,读取文件时发生长时间阻塞,致使后来的请求进入长时间的等待状态。数据库
二、火车站为每个购票用户配备一个临时售票员,这刚开始是很是高效的,但随着购票用户的增长,整个火车站都将被挤爆。从程序角度说,就是每来一个请求,就建立一个线程处理,这样多个请求就能够被并行处理,大大提升的资源使用率和任务处理效率,可是建立线程自己就是消耗资源的,而大量空闲线程将占用了内存(超过上限后会报OutOfMemory异常),也使得cpu在频繁的上下文切换中形成了性能损耗。apache
三、火车站增长多个售票窗口,乘客仍然要排队,但处理效率更高了,哪一个窗口闲了,就处理新的购票申请。这相似于tomcat中的线程池,线程池是用来管理工做线程的,通常和队列配合使用,他对线程进行重复使用,减小了频繁建立线程的消耗,同时能够对线程数量进行控制,在不超过负载的前提下,充分使用内存和cpu资源。tomcat
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。less
咱们再来看下ThreadPoolExecutor构造函数:socket
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 ,任务被拒绝ide
以上规则结合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的内容,之后还会详细讲解。
转载自:https://cloud.tencent.com/developer/article/1033735