注:不能免俗,本文大量借鑒綜合其他文章及圖片,結(jié)尾有備注,在此先行感謝,僅作為自己學(xué)習(xí)總結(jié)用。?那為何還要搞一篇這樣的文章呢,雖然是大篇幅的借鑒,但是也還是有自己的獨立思考,涉及的源碼也是看過的,自己理解了就是好的,當(dāng)然也希望能幫助那些需要的同學(xué),畢竟把那幾篇大佬的文章都拉在一起了,也可以自己去參照。
開篇就是直接復(fù)制的??: )
我們先來簡單回顧下目前一般的NIO服務(wù)器端的大致實現(xiàn),借鑒infoq上的一篇文章Netty系列之Netty線程模型中的一張圖

一個或多個Acceptor線程,每個線程都有自己的Selector,Acceptor只負(fù)責(zé)accept新的連接,一旦連接建立之后就將連接注冊到其他Worker線程中
多個Worker線程,有時候也叫IO線程,就是專門負(fù)責(zé)IO讀寫的。一種實現(xiàn)方式就是像Netty一樣,每個Worker線程都有自己的Selector,可以負(fù)責(zé)多個連接的IO讀寫事件,每個連接歸屬于某個線程。另一種方式實現(xiàn)方式就是有專門的線程負(fù)責(zé)IO事件監(jiān)聽,這些線程有自己的Selector,一旦監(jiān)聽到有IO讀寫事件,并不是像第一種實現(xiàn)方式那樣(自己去執(zhí)行IO操作),而是將IO操作封裝成一個Runnable交給Worker線程池來執(zhí)行,這種情況每個連接可能會被多個線程同時操作,相比第一種并發(fā)性提高了,但是也可能引來多線程問題,在處理上要更加謹(jǐn)慎些。tomcat的NIO模型就是第二種。
Tomcat處理請求過程
先來借鑒看下tomcat高并發(fā)場景下的BUG排查中的一張圖 。主要有幾個組件:
Acceptor線程:全局唯一,負(fù)責(zé)接受請求,并將請求放入Poller線程的事件隊列。Accetpr線程在分發(fā)事件的時候,采用的Round Robin的方式來分發(fā)的
Poller線程:官方的建議是每個處理器配一個,但不要超過兩個,由于現(xiàn)在幾乎都是多核處理器,所以一般來說都是兩個。每個Poller線程各自維護(hù)一個事件隊列(無上限),它的職責(zé)是從事件隊列里面拿出socket,往自己的selector上注冊,然后等待selector選擇讀寫事件,并交給SocketProcessor線程去實際處理請求。
SocketProcessor線程:它是實際的工作線程,用于處理請求。
一個典型的請求處理過程
Acceptor線程接受請求,從socketCache里面拿出socket對象(沒有的話會創(chuàng)建,緩存的目的是避免對象創(chuàng)建的開銷),
Acceptor線程標(biāo)記好Poller對象,組裝成PollerEvent,放入該Poller對象的PollerEvent隊列
Poller線程從事件隊列里面拿出PollerEvent,將其中的socket注冊到自身的selector上,
Poller線程等到有讀寫事件發(fā)生時,分發(fā)給SocketProcessor線程去實際處理請求
SocketProcessor線程處理完請求,socket對象被回收,放入socketCache

以上是總體介紹,我們再來詳細(xì)分析:
Accept Queue
對于client端的一個請求進(jìn)來,流程是這樣的:tcp的三次握手建立連接,建立連接的過程中,OS維護(hù)了半連接隊列(syn隊列)以及完全連接隊列(accept隊列),在第三次握手之后,server收到了client的ack,則進(jìn)入establish的狀態(tài),然后該連接由syn隊列移動到accept隊列。ServerSocketChannel accept就是從這個隊列中不斷取出已經(jīng)建立連接的的請求。所以當(dāng)ServerSocketChannel accept取出不及時就有可能造成該隊列積壓,一旦滿了連接就被拒絕了。
(上面圖并沒有介紹這個queue,也不是直接從queue中拿socket,是由于tomcat作了緩存。從socketCache里面拿出socket對象。沒有的話才創(chuàng)建,緩存的目的是避免對象創(chuàng)建的開銷)
tomat的acceptCount參數(shù)指的就是這個隊列的大小。
acceptCount: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.

Acceptor
tomcat的acceptor線程則負(fù)責(zé)從accept隊列中取出該connection,接受該connection,并轉(zhuǎn)交出去,然后自己接著去accept隊列取connection(當(dāng)當(dāng)前socket連接超過maxConnections的時候,acceptor線程自己會阻塞等待,等連接降下去之后,才去處理accept隊列的下一個連接)。
accept核心有3步:
1.countUpOrAwaitConnection,進(jìn)行連接數(shù)的自增,是在accept新的連接之前判斷,目的就是控制連接數(shù):當(dāng)前socket連接超過maxConnections的時候,acceptor線程自己會阻塞等待,等連接降下去之后,才去處理accept隊列的下一個連接。
當(dāng)連接數(shù)我們設(shè)置maxConnections=-1的時候就表示不用限制最大連接數(shù)。默認(rèn)是限制10000,如果不限制則一旦出現(xiàn)大的沖擊,則tomcat很有可能直接掛掉,導(dǎo)致服務(wù)停止。
2.socket = serverSock.accept(); accept
3.setSocketOptions(socket))? ? //will add channel to the poller?
3.1設(shè)置一些參數(shù) ..
3.2選中一個pooller注冊:getPoller0().register(channel);
我們來看下Acceptor源碼:
protected classAcceptorextendsAbstractEndpoint.Acceptor{
//if we have reached max connections, wait?
?countUpOrAwaitConnection();? ? ? ? 1.
? SocketChannel socket = null;
?try {
????// Accept the next incoming connection from the server? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
????socket = serverSock.accept();? ? ? ? 2.
// setSocketOptions() will add channel to the poller?
?if (!setSocketOptions(socket)) {? ? ? ? 3.
getPoller0().register(channel);? ? ? ? ? ? 3.2
Pooller
1、注冊其實就是將socket和Poller的關(guān)系綁定,再次從緩存中取出或者重新構(gòu)建一個PollerEvent,然后將該event放到Poller的事件隊列中等待被異步處理。
2、在Poller的run方法中不斷處理上述事件隊列中的事件,直接執(zhí)行PollerEvent的run方法,將SocketChannel注冊到自己的Selector上。
3、并將Selector監(jiān)聽到的IO讀寫事件封裝成SocketProcessor,交給線程池執(zhí)行
public void register(finalNioChannel socket){? ? ? ? 1.
? ? socket.setPoller(this);
? ? NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
? ? socket.setSocketWrapper(ka);
? ? ka.setPoller(this);
? ? ka.setReadTimeout(getSocketProperties().getSoTimeout());
? ? ka.setWriteTimeout(getSocketProperties().getSoTimeout());
? ? ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
? ? ka.setSecure(isSSLEnabled());
? ? ka.setReadTimeout(getSoTimeout());
? ? ka.setWriteTimeout(getSoTimeout());
? ? PollerEvent r = eventCache.pop();
? ? ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.? ??
? ?if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
? ? else r.reset(socket,ka,OP_REGISTER);
? ? addEvent(r);
}
private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();
private void addEvent(PollerEvent event){
? ? events.offer(event);
? ? if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();
}
SocketProcessor sc = processorCache.pop();
if ( sc == null ) sc = new SocketProcessor(attachment, status);
else sc.reset(attachment, status);
Executor executor = getExecutor();if (dispatch && executor != null) {
? ? executor.execute(sc);? ? ? ? ? ? ? ? ? ? ?//3.
} else {
? ? sc.run();
}
SocketProcessor?ThreadPool/Executor
這里才到了真正執(zhí)行任務(wù)的線程池:SocketProcessor線程池,其配置就對應(yīng)于我們Tomcat的配置了:
minSpareThreads,maxThreads
最主要的是這個線程池跟Jdk里的線程池還是有些區(qū)別的,據(jù)之前ThreadPoolExecutor的源碼分析,核心線程數(shù)滿了之后,會先將任務(wù)放到隊列中,隊列滿了才會創(chuàng)建出新的非核心線程,如果隊列是一個大容量的話,也就是不會到創(chuàng)建新的非核心線程那一步了。但是這里的TaskQueue修改了底層offer的實現(xiàn)。當(dāng)線程數(shù)小于最大線程數(shù)的時候就直接返回false即入隊列失敗,則迫使ThreadPoolExecutor創(chuàng)建出新的非核心線程。
public void createExecutor(){
? ? TaskQueue taskqueue = new TaskQueue();
? ? executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
}
coreThread:
private int minSpareThreads = 10;
publicintgetMinSpareThreads(){
? ? return Math.min(minSpareThreads,getMaxThreads());
}
maxThread: private int maxThreads = 200;
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);
}
了解這些之后,來發(fā)表下自己的拙見:回過頭來看看這些參數(shù):acceptCount, maxConnections, inSpareThreads, maxThreads,并試圖理一下:
建立連接,放入accept queue,當(dāng)queue已滿直接丟棄!什么時候accept queue會堆積?那就是acceptor沒有及時來取連接。那什么時候acceptor不能及時來取連接?那就是acceptor線程被阻塞了,一般acceptor線程不斷輪訓(xùn)來拿線程是完全不會造成堆積的。那acceptor線程為何會阻塞呢?前面代碼看了,也就是當(dāng)前連接數(shù)已達(dá)到maxConnections。那什么情況下,連接會堆積呢?那就是說明工作線程池(SocketProcessor)處理不過來了(否則其處理完socket是會被回收的),pooller隊列中還堆積了大量的連接,tomat連接數(shù)=pooller隊列中的連接 +?SocketProcessor線程池中正在處理的連接。
至于inSpareThreads, maxThreads就比較好理解了,就是線程池中線程數(shù)。
所以,又反過來梳理一遍正常流程,SocketProcessor 線程池及時處理任務(wù),不斷從pooller中取event,則pooller queue就不會堆積,那么Acceptor就總能及時將連接放入到Pooller queue。。周而復(fù)始。
感覺自己總結(jié)的有點Low啊,睡覺去。。
參考:
https://tomcat.apache.org/tomcat-8.5-doc/config/http.html
https://my.oschina.net/pingpangkuangmo/blog/668925
https://yq.aliyun.com/articles/2889?spm=5176.team22.teamshow1.30.XRi499
https://segmentfault.com/a/1190000008064162
https://www.cnblogs.com/zhanjindong/p/concurrent-and-tomcat-threads-updated.html