咱們以排隊買票爲例子,說說三種方案: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