1. 程式人生 > 其它 >Tomcat處理HTTP請求高併發原理剖析

Tomcat處理HTTP請求高併發原理剖析

技術標籤:筆記tomcat

Tomcat處理HTTP請求過程分析
一、Tomcat是什麼?
Tomcat是一個web應用伺服器,是一個Servlet/Jsp容器,主要負責將客戶端請求傳遞給對應的Servlet,並且將Servlet的響應資料返回給客戶端。
Tomcat是基於元件的伺服器。
二、Tomcat體系結構
Tomcat是一個基於元件的伺服器,它的構成元件都是可配置的。其各個元件都在Tomcat安裝目錄下的…/conf/server.xml檔案中配置。

<?xml version="1.0" encoding="UTF-8"?>

<Resource name=“UserDatabase” auth=“Container”

type=“org.apache.catalina.UserDatabase”

description=“User database that can be updated and saved”

factory=“org.apache.catalina.users.MemoryUserDatabaseFactory”

pathname=“conf/tomcat-users.xml” />

<Connector port=“8080” protocol=“HTTP/1.1”

connectionTimeout=“20000”

redirectPort=“8443” />

<Realm className=“org.apache.catalina.realm.UserDatabaseRealm”

resourceName=“UserDatabase”/>

<Host name=“localhost” appBase=“webapps”

unpackWARs=“true” autoDeploy=“true”>

<Valve className=“org.apache.catalina.valves.AccessLogValve” directory=“logs”

prefix=“localhost_access_log” suffix=".txt"

pattern="%h %l %u %t “%r” %s %b" />

由第一張圖可以看出Tomcat的心臟是兩個核心元件:Connector(聯結器)和Container(容器)。其中一個Container可以選擇多個Connector。
擴充套件:Tomcat預設提供兩個Connector聯結器,一個預設監聽8080埠,一個預設監聽8009埠,這兩種聯結器有什麼區別呢?redirectPort有什麼作用?
(1)8080埠監聽的是通過HTTP/1.1協議訪問的連線,而8009埠主要負責和其他HTTP伺服器(如Apache、IIS)建立連線,使用AJP/1.3協議,當Tomcat和其他伺服器整合時就會使用到這個聯結器。如下圖。

Web1和Web2都是訪問伺服器的index.jsp頁面。Web1直接訪問Tomcat伺服器,訪問地址是http://localhost:8080/index.jsp。Web2訪問HTTP伺服器,HTTP伺服器再通過訪問Tomcat的8009埠找到index.jsp。假設HTTP伺服器的埠為80埠,則訪問地址為http://localhost:80/index.jsp 或者http://localhost/index.jsp。
Apache、IIS伺服器一般只支援靜態頁面,如HTML,不支援JSP動態頁面。Tomcat對HTML的解析速度不如Apache、IIS伺服器。因此一般將兩者整合使用。
(2)redirectPort字面意思是重定向埠。當用戶用http請求某個資源,而該資源本身又被設定了必須要https方式訪問,此時Tomcat會自動重定向到這個redirectPort設定的https埠。
三、元件
1、Connector元件

Connector 最重要的功能就是接收連線請求然後分配執行緒讓 Container來處理這個請求,所以Connector必然是多執行緒的,多執行緒的處理是 Connector 設計的核心。Connector監聽指定埠上請求,當請求到來時建立一個request和response物件交換資料,然後新建一個執行緒來處理請求並把request和response傳遞給Engine元件,最後從Engine獲取一個響應並返回給客戶端。
Connector元件常用屬性說明:
(1)address:指定聯結器監聽的地址,預設為所有地址,即0.0.0.0,可以自己指定地。(2)maxThreads:支援的最大併發連線數,預設為200;
(3)port:監聽的埠;
(4)protocol:聯結器使用的協議,預設為HTTP/1.1,定義AJP協議時通常為AJP/1.3;
(5)redirectPort:如果某聯結器支援的協議是HTTP,當接收客戶端發來的HTTPS請求時,則轉發至此屬性定義的埠;
(6)connectionTimeout:等待客戶端傳送請求的超時時間,單位為毫秒,預設為60000,即1分鐘;
(7)enableLookups:是否通過request.getRemoteHost()進行DNS查詢以獲取客戶端的主機名;預設為true; 進行反解的,可以設定為false。
(8)acceptCount:設定等待佇列的最大長度;通常在tomcat所有處理執行緒均處於繁忙狀態時,新發來的請求將被放置於等待佇列中;

2.container元件

Container是容器的父介面,該容器的設計用的是典型的責任鏈的設計模式,它由四個子容器元件構成,分別是Engine、Host、Context、Wrapper。這四個元件是負責關係,存在包含關係。其中Engine是最頂層,每個service 最多隻能有一個Engine, Engine 裡面可以有多個Host ,每個Host 下可以有多個Context ,每個Context 下可以有多個Wrapper,通常一個Servlet class對應一個Wrapper,如果有多個Servlet則定義多個Wrapper,如果有多個Wrapper就要定義一個更高的Container,如Context。 Context定義在父容器 Host 中,其中Host 不是必須的,但是要執行 war 程式,就必須要 Host,因為 war 中必有 web.xml 檔案,這個檔案的解析就需要 Host 了,如果要有多個 Host 就要定義一個 top 容器 Engine 了。而 Engine 沒有父容器了,一個 Engine 代表一個完整的 Servlet 引擎。
2.1、Engine
Engine是Servlet處理器的一個例項,即servlet引擎,一個Service 最多隻能有一個Engine。預設為定義在server.xml中的Catalina。Engine需要defaultHost屬性來為其定義一個接收所有請求的虛擬主機host元件。
2.2、Host
Host是Engine的子容器。一個 Host 在 Engine 中代表一個站點,也叫虛擬主機,這個虛擬主機的作用就是執行多個應用、接收並處理請求、儲存一個主機應該有的資訊。
常用屬性說明:
(1)appBase:此Host的webapps目錄,專案存放路徑,可以使用絕對路徑;
(2)autoDeploy:在Tomcat處於執行狀態時放置於appBase目錄中的應用程式檔案是否自動進行deploy;預設為true;
(3)unpackWars:在啟用此webapps時是否對WAR格式的歸檔檔案先進行展開;預設為true;
2.3、Context
Context :代表一個應用程式,對應著平時開發的一套程式,或者一個WEB-INF 目錄以及下面的web.xml 檔案。它具備了 Servlet 執行的基本環境,理論上只要有 Context 就能執行 Servlet 了。簡單的 Tomcat 可以沒有 Engine 和 Host。Context 最重要的功能就是管理它裡面的 Servlet 例項,Servlet 例項在 Context 中是以 Wrapper 出現的,還有一點就是 Context 如何才能找到正確的 Servlet 來執行它呢? Tomcat5 以前是通過一個 Mapper 類來管理的,Tomcat5 以後這個功能被移到了 request 中,獲取子容器都是通過 request 來分配的。
常用屬性定義:
(1)docBase:相應的Web應用程式的存放位置;也可以使用相對路徑,起始路徑為此Context所屬Host中appBase定義的路徑;切記,docBase的路徑名不能與相應的Host中appBase中定義的路徑名有包含關係,比如,如果appBase為deploy,而docBase絕不能為deploy-bbs類的名字;
(2)path:相對於Web伺服器根路徑而言的URI;如果為空“”,則表示為此webapp的根路徑;如果context定義在一個單獨的xml檔案中,此屬性不需要定義,有可能是別名;
(3)reloadable:是否允許重新載入此context相關的Web應用程式的類;預設為false;
2.4、Wrapper
Wrapper :每個Wrapper 封裝著一個servlet,也代表一個 Servlet,它負責管理一個 Servlet,包括Servlet 的裝載、初始化、執行以及資源回收。Wrapper 是最底層的容器,它沒有子容器了,所以呼叫它的 addChild 將會報錯。 Wrapper 的實現類是 StandardWrapper,StandardWrapper 還實現了 ServletConfig,由此看出 StandardWrapper 將直接和 Servlet 的各種資訊打交道。
2.5、Value
Valve類似於過濾器,它可以工作於Engine和Host/Context之間、Host和Context之間以及Context和Web應用程式的某資源之間。一個容器內可以建立多個Valve,而且Valve定義的次序也決定了它們生效的次序。

四、Tomcat處理一個HTTP請求的過程

1.使用者在瀏覽器中輸入網址localhost:8080/test/index.jsp,請求被髮送到本機埠8080,被在那裡監聽的Coyote HTTP/1.1 Connector獲得;
2.Connector把該請求交給它所在的Service的Engine(Container)來處理,並等待Engine的迴應;
3.Engine獲得請求localhost/test/index.jsp,匹配所有的虛擬主機Host;
4.Engine匹配到名為localhost的Host(即使匹配不到也把請求交給該Host處理,因為該Host被定義為該Engine的預設主機)。名為localhost的Host獲得請求/test/index.jsp,匹配它所擁有的所有Context。Host匹配到路徑為/test的Context(如果匹配不到就把該請求交給路徑名為“”的Context去處理);
5.path=“/test”的Context獲得請求/index.jsp,在它的mapping table中尋找出對應的Servlet。Context匹配到URL Pattern為*.jsp的Servlet,對應於JspServlet類;
6.構造HttpServletRequest物件和HttpServletResponse物件,作為引數呼叫JspServlet的doGet()或doPost(),執行業務邏輯、資料儲存等;
7.Context把執行完之後的HttpServletResponse物件返回給Host;
8.Host把HttpServletResponse物件返回給Engine;
9.Engine把HttpServletResponse物件返回Connector;
10.Connector把HttpServletResponse物件返回給客戶Browser。
五、Tomcat是如何將請求一步步傳遞到我們編寫的HttpServlet類中的
1.先建立一個簡單的動態WEB工程,然後寫一個HttpServlet的實現類。程式碼如下
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet
{
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
System.out.println(“這個doGet方法”);
request.getRequestDispatcher("/test.jsp").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
}
}
編寫一個訪問頁面,index.jsp.

主頁

Test 一個響應頁面。

成功響應

二、請求的需要經過哪些地方。   請求資訊會經過以下的介面(java類)及其子類來處理,就相當於要經過許多模組來處理。

三、具體步驟
  如上圖,請求在伺服器的處理經過:
①、AbstractEndpoint類及其子類來處理。

AbstractEndpoint這個抽象類中有一個抽象內部類Acceptor,這個Acceptor的實現類是AbstractEndpoint的三個子類的內部類Acceptor來實現的。

我們的請求就是被這Acceptor監聽到並且接收的。這個類其實是一個執行緒類,因為AbstractEndpoint.Acceptor實現了Runnable介面。

@Override
public void run() {
        int errorDelay = 0;

        // running表示endpoint的狀態,在run()方法中這個while迴圈,會一直迴圈接收資料,直到endpoint 不處於執行狀態。
        while (running) {

            // 當endpoint暫停,但還是執行狀態就讓執行緒睡眠。
            while (paused && running) {
                state = AcceptorState.PAUSED;
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // Ignore                    }
            }

//endpoint不處於執行狀態,就停止接收資料。
if (!running) {
break;
}
state = AcceptorState.RUNNING;
          
            
          //下面就是的接收資料的過程。
try {
//if we have reached max connections, wait countUpOrAwaitConnection();

                long socket = 0;
                try {
                    // 從server sock 接收傳進來的連線。
                    socket = Socket.accept(serverSock);
                    if (log.isDebugEnabled()) {
                        long sa = Address.get(Socket.APR_REMOTE, socket);
                        Sockaddr addr = Address.getInfo(sa);
                        log.debug(sm.getString("endpoint.apr.remoteport",
                                Long.valueOf(socket),
                                Long.valueOf(addr.port)));
                    }
                } catch (Exception e) {
          //省略了部分處理異常的程式碼。// The processor will recycle itself when it finishes            }

state = AcceptorState.ENDED;
}

以上就是AprEndpoint接收請求的過程。就是用一個接收器接收請求,過程中會使用套接字。但是好像並不是有的請求都會用這個Acceptor來接收。
當接收請求完畢,經過一系列的處理後就會由AprEndpoint的內部類SocketProcessor來將請求傳給ProtocolHandler來處理。這個SocketProcessor也是一個執行緒類。
它有一行程式碼將套接字傳給了第二步來處理。
程式碼中的handler就是AbstractProtocol中的內部類AbstractConnectionHandler的例項,這樣,套接字就被傳到第②步了。

②、在AbstractConnectionHandler接收到第一步傳來的套接字以後,對套接字進行處理,下面是它進行處理的程式碼。

    @SuppressWarnings("deprecation") // Old HTTP upgrade method has been deprecated
    public SocketState process(SocketWrapper<S> wrapper,
            SocketStatus status) {

//省略部分程式碼

//這是第三步中Processor介面的實現類。connections是一個Map物件,套接字為鍵,Processor介面實現類例項為值。
Processor processor = connections.get(socket);
if (status == SocketStatus.DISCONNECT && processor == null) {
// Nothing to do. Endpoint requested a close and there is no
// longer a processor associated with this socket.
return SocketState.CLOSED;
}

        wrapper.setAsync(false);
        ContainerThreadMarker.markAsContainerThread();

        try {
            if (processor == null) {
                processor = recycledProcessors.poll();
            }
            if (processor == null) {
                processor = createProcessor();
            }

            initSsl(wrapper, processor);

            SocketState state = SocketState.CLOSED;
            do {
                if (status == SocketStatus.DISCONNECT &&
                        !processor.isComet()) {
                    // Do nothing here, just wait for it to get recycled
                    // Don't do this for Comet we need to generate an end
                    // event (see BZ 54022)
                } else if (processor.isAsync() || state == SocketState.ASYNC_END) {
                    state = processor.asyncDispatch(status);
                    if (state == SocketState.OPEN) {
                        // release() won't get called so in case this request
                        // takes a long time to process, remove the socket from
                        // the waiting requests now else the async timeout will
                        // fire                            getProtocol().endpoint.removeWaitingRequest(wrapper);
                        // There may be pipe-lined data to read. If the data
                        // isn't processed now, execution will exit this
                        // loop and call release() which will recycle the
                        // processor (and input buffer) deleting any
                        // pipe-lined data. To avoid this, process it now.

//wapper就是套接字包裝類的物件,這裡還是理解為套接字,套接字在這裡傳給了第③步的Processor介面的例項。
state = processor.process(wrapper);
}
} else if (processor.isComet()) {
state = processor.event(status);
} else if (processor.getUpgradeInbound() != null) {
state = processor.upgradeDispatch();
} else if (processor.isUpgrade()) {
state = processor.upgradeDispatch(status);
} else {
              //註釋同上,
state = processor.process(wrapper);
}

                if (state != SocketState.CLOSED && processor.isAsync()) {
                    state = processor.asyncPostProcess();                //省略掉部分程式碼
        } catch(java.net.SocketException e) {             //將部分捕獲異常的程式碼省略掉

// Make sure socket/processor is removed from the list of current
// connections connections.remove(socket);
// Don’t try to add upgrade processors back into the pool
if (!(processor instanceof org.apache.coyote.http11.upgrade.UpgradeProcessor)
&& !processor.isUpgrade()) {
release(wrapper, processor, true, false);
}
return SocketState.CLOSED;
}

③、第二步完成後,就會交給Processor介面的實現類來處理。在AbstractHttp11Processor中的process(…)方法來處理.部分程式碼如下:
在這裡將會建立請求和響應,但不是我們熟悉的HttpServletRequest或HttpServletResponse型別或其子型別。而是
org.apache.coyote.Request 和 org.apache.coyote.Response型別的,這個請求什麼時候會變成的HttpServletRequest型別的呢??它將會在Connector中來進行處理,我說的是處理而不是型別轉換是因為org.apache.coyote.Request 和HttpServletRequest並不是父子關係的類,總之,HttpServletRequest的請求是由Connector來建立,在CoyoteAdapter中處理成HttpServletRequest.
這個方法就是請求物件、響應物件和套接字進行資訊互動的地方,也就是真真正正將套接字中的資訊轉化為請求資訊,還要把響應資訊寫到套接字中。
由於只討論請求的傳遞,所以我將方法的大部分程式碼刪掉了。

@Override
public SocketState process(SocketWrapper<S> socketWrapper)
    throws IOException {

//省略部分程式碼
// Process the request in the adapter
if (!getErrorState().isError()) {
try {
rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);
            //請求和響應在這個類中被建立並將套接字中的資訊寫入的請求中。在這裡,請求將會被傳遞到第④步。
            //這個adapter就是CoyoteAdapter的一個例項
            adapter.service(request, response);
// Handle when the response was committed before a serious
// error occurred. Throwing a ServletException should both
// set the status to 500 and set the errorException.
// If we fail here, then the response is likely already
// committed, so we can’t try and set headers.
if(keepAlive && !getErrorState().isError() && (
response.getErrorException() != null ||
(!isAsync() &&
statusDropsConnection(response.getStatus())))) {
setErrorState(ErrorState.CLOSE_CLEAN, null);
}
setCometTimeouts(socketWrapper);
} catch (InterruptedIOException e) {
//省略部分程式碼
}
}

④、第三步完成之後交給CoyoteAdapter來處理,CoyotoAdapter是將請求傳入Server容器的切入點。
我一直不明白coyote是什麼意思,有道上說是一種產於北美大草原的小狼。。。感覺不太能理解。所以我把Adapter介面的類註釋貼上來,只有一句簡單的話,如下:
Adapter. This represents the entry point in a coyote-based servlet container.
CoyoteAdapter中有一個service()方法。這個方法持有一個Connector的引用。這個Connector又持有一個Service容器的引用,而Service容器有持有一個Container(Container的實現類有StandardEngine、StandardHost等等)的引用。所以CoyoteAdapter就可以根據這些引用將請求傳遞到Server容器中了。如下圖中的程式碼:

還是將CoyoteAdapter的service(…)方法的程式碼帖出來一下吧。

/**
 * Service method.
 */
@Override
public void service(org.apache.coyote.Request req,
                    org.apache.coyote.Response res)
    throws Exception {

//下面這兩行程式碼就是將請求處理後轉化為HttpServletRequest的
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
      //如果請求為null就讓Connector來建立。
if (request == null) {

        // Create objects
        request = connector.createRequest();
        request.setCoyoteRequest(req);
        response = connector.createResponse();
        response.setCoyoteResponse(res);

//省略部分程式碼
}

    if (connector.getXpoweredBy()) {
        response.addHeader("X-Powered-By", POWERED_BY);
    }

    boolean comet = false;
    boolean async = false;
    boolean postParseSuccess = false;

    try {
        // Parse and set Catalina and configuration specific
        // request parameters            req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName());
        postParseSuccess = postParseRequest(req, request, res, response);
        if (postParseSuccess) {
            //check valves if we support async                request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported());
            // Calling the container 在這裡將請求傳到Server容器中。
            connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

          //省略部分程式碼

        }
        
}    /**

⑤、如果上面的請求傳遞到的Container是StandaradEngine,那麼就會Engine就會呼叫它持有的StandardPipeline物件來處理請求。StandardPipeline就相當於一條管道,這條管道中的有許多閥門,這些閥門會對請求進行處理,並且控制它下一步往哪裡傳遞。StandardEngine的管道使用的閥門是StandardEngineValve。
⑥、和StandardEngine一樣,StandardHost、StandardContext、StandardWrapper這幾個容器都擁有自己的一條管道StandardPipeline來處理的請求。但是需要注意的是他們使用的閥門是不一樣的。StandardHost則會使用StandardHostValve,其他的同理。。。。。。。
⑦、當最後一個StandardWrapperVale處理完請求後,此時這個請求會如何傳遞呢??此時請求已經到達了最底層的容器了。StandardWrapper就是最底層的容器,它不允許再有子容器。其實每一個StandardWrapper代表一個Servlet,因為每一個StandardWrapper都會持有一個Servlet例項的引用。閥門會將請求交給誰???看一張圖:

這是debug的截圖,右上圖可知,當最後一個StandardWrapperValve處理完請求以後,把請求交給Filter來處理,此時請求進入過濾器鏈條中,也就是我們熟悉 filter chain。
⑧、當過濾器處理完之後當然是將請求傳遞給我們編寫的HttpServlet來處理了。

六、執行緒池的原理和Tomcat的Connector及執行緒池配置

它的流程如下:先啟動若干數量的執行緒,並讓這些執行緒都處於睡眠 狀態,當客戶端有一個新請求時,就會喚醒執行緒池中的某一個睡眠執行緒,讓它來處理客戶端的這個請求,當處理完這個請求後,執行緒又處於睡眠狀態。可能你也許會 問:為什麼要搞得這麼麻煩,如果每當客戶端有新的請求時,我就建立一個新的執行緒不就完了?這也許是個不錯的方法,因為它能使得你編寫程式碼相對容易一些,但 你卻忽略了一個重要的問題??效能!例如:一個省級資料大集中的銀行網路中心,高峰期每秒的客戶端請求併發數超過100,如果 為每個客戶端請求建立一個新執行緒的話,那耗費的CPU時間和記憶體將是驚人的,如果採用一個擁有200個執行緒的執行緒池,那將會節約大量的的系統資源,使得更 多的CPU時間和記憶體用來處理實際的商業應用,而不是頻繁的執行緒建立與銷燬。
配置executor屬性(各項引數值根據自身情況配置)
1.1)開啟/conf/server.xml檔案,在Connector之前配置一個執行緒池:

引數詳解:
name:共享執行緒池的名字。這是Connector為了共享執行緒池要引用的名字,該名字必須唯一。預設值:None;
namePrefix:在JVM上,每個執行執行緒都可以有一個name 字串。這一屬性為執行緒池中每個執行緒的name字串設定了一個字首,Tomcat將把執行緒號追加到這一字首的後面。預設值:tomcat-exec-;
maxThreads:該執行緒池可以容納的最大執行緒數。預設值:200;
maxIdleTime:在Tomcat關閉一個空閒執行緒之前,允許空閒執行緒持續的時間(以毫秒為單位)。只有當前活躍的執行緒數大於minSpareThread的值,才會關閉空閒執行緒。預設值:60000(一分鐘)。
minSpareThreads:Tomcat應該始終開啟的最小不活躍執行緒數。預設值:25。
配置Connector

引數詳解:
executor:表示使用該引數值對應的執行緒池;
minProcessors:伺服器啟動時建立的處理請求的執行緒數;
maxProcessors:最大可以建立的處理請求的執行緒數;
acceptCount:指定當所有可以使用的處理請求的執行緒數都被使用時,可以放到處理佇列中的請求數,超過這個數的請求將不予處理。
六、tomcat執行緒池和jdk執行緒池的關係
前言
Tomcat/Jetty 是目前比較流行的 Web 容器,兩者接受請求之後都會轉交給執行緒池處理,這樣可以有效提高處理的能力與併發度。JDK 提高完整執行緒池實現,但是 Tomcat/Jetty 都沒有直接使用。Jetty 採用自研方案,內部實現QueuedThreadPool執行緒池元件,而 Tomcat 採用擴充套件方案,踩在 JDK 執行緒池的肩膀上,擴充套件 JDK 原生執行緒池。
JDK 原生執行緒池可以說功能比較完善,使用也比較簡單,那為何 Tomcat/Jetty 卻不選擇這個方案,反而自己去動手實現那?
JDK 執行緒池
通常我們可以將執行的任務分為兩類:
cpu 密集型任務
io 密集型任務
cpu 密集型任務,需要執行緒長時間進行的複雜的運算,這種型別的任務需要少建立執行緒,過多的執行緒將會頻繁引起上文切換,降低任務處理處理速度。
而 io 密集型任務,由於執行緒並不是一直在執行,可能大部分時間在等待 IO 讀取/寫入資料,增加執行緒數量可以提高併發度,儘可能多處理任務。
JDK 原生執行緒池工作流程如下:

上圖假設使用LinkedBlockingQueue。
靈魂拷問:上述流程是否記錯過?在很長一段時間內,我都認為執行緒數量到達最大執行緒數,才放入佇列中。 ̄□ ̄||
上圖中可以發現只要執行緒池執行緒數量大於核心執行緒數,就會先將任務加入到任務佇列中,只有任務佇列加入失敗,才會再新建執行緒。也就是說原生執行緒池佇列未滿之前,最多隻有核心執行緒數量執行緒。
這種策略顯然比較適合處理cpu密集型任務,但是對於io密集型任務,如資料庫查詢,rpc 請求呼叫等,就不是很友好了。
由於 Tomcat/Jetty 需要處理大量客戶端請求任務,如果採用原生執行緒池,一旦接受請求數量大於執行緒池核心執行緒數,這些請求就會被放入到佇列中,等待核心執行緒處理。這樣做顯然降低這些請求總體處理速度,所以兩者都沒采用 JDK 原生執行緒池。
解決上面的辦法可以像 Jetty 自己實現執行緒池元件,這樣就可以更加適配內部邏輯,不過開發難度比較大,另一種就像 Tomcat 一樣,擴充套件原生 JDK 執行緒池,實現比較簡單。
下面主要以 Tomcat 擴充套件執行緒池,講講其實現原理。
擴充套件執行緒池
首先我們從 JDK 執行緒池原始碼出發,檢視如何這個基礎上擴充套件。

可以看到執行緒池流程主要分為三步,第二步根據queue#offer方法返回結果,判斷是否需要新建執行緒。
JDK 原生佇列型別LinkedBlockingQueue,SynchronousQueue,兩者實現邏輯不盡相同。
LinkedBlockingQueue
offer方法內部將會根據佇列是否已滿作為判斷條件。若佇列已滿,返回false,若佇列未滿,則將任務加入佇列中,且返回true。
SynchronousQueue
這個佇列比較特殊,內部不會儲存任何資料。若有執行緒將任務放入其中將會被阻塞,直到其他執行緒將任務取出。反之,若無其他執行緒將任務放入其中,該佇列取任務的方法也將會被阻塞,直到其他執行緒將任務放入。
對於offer方法來說,若有其他執行緒正在被取方法阻塞,該方法將會返回true。反之,offer 方法將會返回 false。
所以若想實現適合 io 密集型任務執行緒池,即優先新建執行緒處理任務,關鍵在於queue#offer方法。可以重寫該方法內部邏輯,只要當前執行緒池數量小於最大執行緒數,該方法返回false,執行緒池新建執行緒處理。
當然上述實現邏輯比較糙,下面我們就從 Tomcat 原始碼檢視其實現邏輯。
Tomcat 擴充套件執行緒池
Tomcat 擴充套件執行緒池直接繼承 JDK 執行緒池java.util.concurrent.ThreadPoolExecutor,重寫部分方法的邏輯。另外還實現了TaskQueue,直接繼承LinkedBlockingQueue,重寫offer方法。
首先檢視 Tomcat 執行緒池的使用方法。

可以看到 Tomcat 執行緒池使用方法與普通的執行緒池差不太多。
接著我們檢視一下 Tomcat 執行緒池核心方法execute的邏輯。

execute方法邏輯比較簡單,任務核心還是交給 Java 原生執行緒池處理。這裡主要增加一個重試策略,如果原生執行緒池執行拒絕策略的情況,丟擲RejectedExecutionException異常。這裡將會捕獲,然後重新再次嘗試將任務加入到TaskQueue,盡最大可能執行任務。
這裡需要注意submittedCount變數。這是 Tomcat 執行緒池內部一個重要的引數,它是一個AtomicInteger變數,將會實時統計已經提交到執行緒池中,但還沒有執行結束的任務。也就是說submittedCount等於執行緒池佇列中的任務數加上執行緒池工作執行緒正在執行的任務。TaskQueue#offer將會使用該引數實現相應的邏輯。
接著我們主要檢視TaskQueue#offer方法邏輯。

核心邏輯在於第三步,這裡如果submittedCount小於當前執行緒池執行緒數量,將會返回false。上面我們講到offer方法返回false,執行緒池將會直接建立新執行緒。
Dubbo 2.6.X 版本增加EagerThreadPool,其實現原理與 Tomcat 執行緒池差不多,感興趣的小夥伴可以自行翻閱。
折衷方法
上述擴充套件方法雖然看起不是很難,但是自己實現代價可能就比較大。若不想擴充套件執行緒池執行 io 密集型任務,可以採用下面這種折衷方法。
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(100));
不過使用這種方式將會使keepAliveTime失效,執行緒一旦被建立,將會一直存在,比較浪費系統資源。
總結
JDK 實現執行緒池功能比較完善,但是比較適合執行 CPU 密集型任務,不適合 IO 密集型的任務。對於 IO 密集型任務可以間接通過設定執行緒池引數方式做到。