一個或多個acceptor線程,每個線程都有自己的selector,acceptor隻負責accept新的連接配接,一旦連接配接建立之後就将連接配接注冊到其他worker線程中
多個worker線程,有時候也叫io線程,就是專門負責io讀寫的。一種實作方式就是像netty一樣,每個worker線程都有自己的selector,可以負責多個連接配接的io讀寫事件,每個連接配接歸屬于某個線程。另一種方式實作方式就是有專門的線程負責io事件監聽,這些線程有自己的selector,一旦監聽到有io讀寫事件,并不是像第一種實作方式那樣(自己去執行io操作),而是将io操作封裝成一個runnable交給worker線程池來執行,這種情況每個連接配接可能會被多個線程同時操作,相比第一種并發性提高了,但是也可能引來多線程問題,在處理上要更加謹慎些。tomcat的nio模型就是第二種。
是以一般參數就是acceptor線程個數,worker線程個數。來具體看下參數
文檔描述為:
the maximum queue length for incoming connection requests when all possible request processing threads are in use. any requests received when the queue is full will be refused. the default value is 100.
這個參數就立馬牽涉出一塊大内容:tcp三次握手的詳細過程,這個之後再詳細探讨。這裡可以簡單了解為:連接配接在被serversocketchannel accept之前就暫存在這個隊列中,acceptcount就是這個隊列的最大長度。serversocketchannel accept就是從這個隊列中不斷取出已經建立連接配接的的請求。是以當serversocketchannel accept取出不及時就有可能造成該隊列積壓,一旦滿了連接配接就被拒絕了
文檔如下描述
the number of threads to be used to accept connections. increase this value on a multi cpu machine, although you would never really need more than 2. also, with a lot of non keep alive connections, you might want to increase this value as well. default value is 1.
acceptor線程隻負責從上述隊列中取出已經建立連接配接的請求。在啟動的時候使用一個serversocketchannel監聽一個連接配接端口如8080,可以有多個acceptor線程并發不斷調用上述serversocketchannel的accept方法來擷取新的連接配接。參數acceptorthreadcount其實使用的acceptor線程的個數。
文檔描述如下
the maximum number of connections that the server will accept and process at any given time. when this number has been reached, the server will accept, but not process, one further connection. this additional connection be blocked until the number of connections being processed falls below maxconnections at which point the server will start accepting and processing new connections again. note that once the limit has been reached, the operating system may still accept connections based on the acceptcount setting. the default value varies by connector type. for nio and nio2 the default is 10000. for apr/native, the default is 8192.
note that for apr/native on windows, the configured value will be reduced to the highest multiple of 1024 that is less than or equal to maxconnections. this is done for performance reasons. if set to a value of -1, the maxconnections feature is disabled and connections are not counted.
這裡就是tomcat對于連接配接數的一個控制,即最大連接配接數限制。一旦發現目前連接配接數已經超過了一定的數量(nio預設是10000),上述的acceptor線程就被阻塞了,即不再執行serversocketchannel的accept方法從隊列中擷取已經建立的連接配接。但是它并不阻止新的連接配接的建立,新的連接配接的建立過程不是acceptor控制的,acceptor僅僅是從隊列中擷取建立立的連接配接。是以當連接配接數已經超過maxconnections後,仍然是可以建立新的連接配接的,存放在上述acceptcount大小的隊列中,這個隊列裡面的連接配接沒有被acceptor擷取,就處于連接配接建立了但是不被處理的狀态。當連接配接數低于maxconnections之後,acceptor線程就不再阻塞,繼續調用serversocketchannel的accept方法從acceptcount大小的隊列中繼續擷取新的連接配接,之後就開始處理這些新的連接配接的io事件了
the maximum number of request processing threads to be created by this connector, which therefore determines the maximum number of simultaneous requests that can be handled. if not specified, this attribute is set to 200. if an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool.
這個簡單了解就算是上述worker的線程數,下面會詳細的說明。他們專門用于處理io事件,預設是200。
上面參數僅僅是簡單了解了下參數配置,下面我們就來詳細研究下tomcat的nio伺服器具體情況,這就要詳細了解下tomcat的nioendpoint實作了
這張圖勾畫出了nioendpoint的大緻執行流程圖,worker線程并沒有展現出來,它是作為一個線程池不斷的執行io讀寫事件即socketprocessor(一個runnable),即這裡的poller僅僅監聽socket的io事件,然後封裝成一個個的socketprocessor交給worker線程池來處理。下面我們來詳細的介紹下nioendpoint中的acceptor、poller、socketprocessor
擷取指定的acceptor數量的線程
可以看到就是一個while循環,循環裡面不斷的accept新的連接配接。
先來看下在accept新的連接配接之前,首選進行連接配接數的自增,即countuporawaitconnection
當我們設定maxconnections=-1的時候就表示不用限制最大連接配接數。預設是限制10000,如果不限制則一旦出現大的沖擊,則tomcat很有可能直接挂掉,導緻服務停止。
這裡的需求就是目前連接配接數一旦超過最大連接配接數maxconnections,就直接阻塞了,一旦目前連接配接數小于最大連接配接數maxconnections,就不再阻塞,我們來看下這個功能的具體實作latch.countuporawait()
具體看這個需求無非就是一個共享鎖,來看具體實作:
目前實作裡算是使用了2個鎖,limitlatch本身的aqs實作再加上atomiclong的aqs實作。也可以不使用atomiclong來實作。
共享鎖的tryacquireshared實作中,如果不依托atomiclong,則需要進行for循環加cas的自增,自增之後沒有超過limit這裡即maxconnections,則直接傳回1表示擷取到了共享鎖,如果一旦超過limit則首先進行for循環加cas的自減,然後傳回-1表示擷取鎖失敗,便進入加入同步隊列進入阻塞狀态。
共享鎖的tryreleaseshared實作中,該方法可能會被并發執行,是以釋放共享鎖的時候也是需要for循環加cas的自減
上述的for循環加cas的自增、for循環加cas的自減的實作全部被替換成了atomiclong的incrementandget和decrementandget而已。
上文我們關注的latch.countuporawait()方法其實就是在擷取一個共享鎖,如下:
然後我們來看下,一個socketchannel連接配接被accept擷取之後如何來處理的呢?
處理過程如下:
設定非阻塞,以及其他的一些參數如sotimeout、receivebuffersize、sendbuffersize
選擇一個poller進行注冊
下面就來詳細介紹下poller
前面沒有說到poller的數量控制,來看下
如果不設定的話最大就是2
來詳細看下getpoller0().register(channel):
就是輪訓一個poller來進行socketchannel的注冊
這裡又是進行一些參數包裝,将socket和poller的關系綁定,再次從緩存中取出或者重新建構一個pollerevent,然後将該event放到poller的事件隊列中等待被異步處理
在poller的run方法中不斷處理上述事件隊列中的事件,直接執行pollerevent的run方法,将socketchannel注冊到自己的selector上。
并将selector監聽到的io讀寫事件封裝成socketprocessor,交給線程池執行
我們來看看這個線程池的初始化:
就是建立了一個threadpoolexecutor,那我們就重點關注下核心線程數、最大線程數、任務隊列等資訊
核心線程數最大是10個,再來看下最大線程數
預設就是上面的配置參數maxthreads為200。還有就是taskqueue,這裡的taskqueue是linkedblockingqueue<runnable>的子類,最大容量就是integer.max_value,根據之前threadpoolexecutor的源碼分析,核心線程數滿了之後,會先将任務放到隊列中,隊列滿了才會建立出新的非核心線程,如果隊列是一個大容量的話,也就是不會到建立新的非核心線程那一步了。
但是這裡的taskqueue修改了底層offer的實作
這裡當線程數小于最大線程數的時候就直接傳回false即入隊列失敗,則迫使threadpoolexecutor建立出新的非核心線程。
taskqueue這一塊沒太看懂它的意圖是什麼,有待繼續研究。
本篇文章描述了tomcat8.5中的nio線程模型,以及其中涉及到的相關參數的設定。下一篇簡單整理下tomcat的整體架構圖