這兩年,tomcat慢慢在新項目裏不怎麼接觸了,由於都被spring boot之類的框架封裝進了內部,成了內置server,不用像過去那樣打個war包,再放到tomcat裏部署了。java
可是,內部的機制咱們仍是有必要了解的,尤爲是線程模型和classloader,這篇咱們會聚焦線程模型。redis
其實我本打算將一個問題,即你們知道,咱們平時最終寫的controller、service那些業務代碼,最終是由什麼線程來執行的呢?spring
你們都是debug過的人,確定知道,線程名稱大概以下:數據庫
http-nio-8080-exec-2@5076
這個線程是tomcat的線程,假設,咱們在這個線程裏,sleep個1分鐘,模擬調用第三方服務時,第三方服務異常卡住不返回的狀況,此時客戶端每秒100個請求過來,此時整個程序會出現什麼狀況?後端
可是我發現,這個問題,一篇仍是講不太清楚,所以,本篇只講一下線程模型。tomcat
你們能夠思考下,一個服務端程序,有哪些是確定須要的?app
咱們確定須要開啓監聽對吧,你們看看下面的bio程序:框架
這個就是個線程,在while(true)死循環裏,一直accept客戶端鏈接。異步
ok,這個線程確定是須要的。接下來,再看看仍是否須要其餘的線程。socket
若是一切從簡,咱們只用這1個線程也足夠了,就像redis同樣,redis都是內存操做,作啥都很快,還避免了線程切換的開銷;
可是咱們的java後端,通常都要操做數據庫的,這個是比較慢,天然是但願把這部分工做可以交給單獨的線程去作,在tomcat裏,確實是這樣的,交給了一個線程池,線程池裏的線程,就是咱們平時看到的,名稱相似http-nio-8080-exec-2@5076這樣的,通常默認配置,最大200個線程。
但若是這樣的話,1個acceptor + 一個業務線程池,會致使一個問題,就是,該acceptor既要負責新鏈接的接入,還要負責已接入鏈接的socket的io讀寫。假設咱們維護了10萬個鏈接,這10萬個鏈接都在不斷地給咱們的服務端發數據,咱們服務端也在不停地給客戶端返回數據,那這個工做仍是很繁重的,可能會壓垮這個惟一的acceptor線程。
所以,理想狀況下,咱們會在單獨弄幾個線程出來,負責已經接入的鏈接的io讀寫。
大致流程:
acceptor--->poller線程(負責已接入鏈接的io讀寫)-->業務線程池(http-nio-8080-exec-2@5076)
這個大概就是tomcat中的流程了。
在netty中,實際上是相似的:
boss eventloop--->worker eventloop-->通常在解碼完成後的最後一個handler,交給自定義業務線程池
你們能夠看看下圖,這裏面有幾個橙色的方塊,這幾個表明了線程,從左到右,分別就是acceptor、nio線程池、poller線程。
1處,acceptor線程內部維護了一個endpoint對象,這個對象呢,就表明了1個服務端端點;該對象有幾個實現類,以下:
咱們spring boot程序裏,默認是用的NioEndpoint。
2處,將新鏈接交給NioEndpoint處理
@Override protected boolean setSocketOptions(SocketChannel socket) { // Process the connection try { // Disable blocking, polling will be used socket.configureBlocking(false); Socket sock = socket.socket(); socketProperties.setProperties(sock); // 進行一些socket的參數設置 NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this); channel.setSocketWrapper(socketWrapper); socketWrapper.setReadTimeout(getConnectionTimeout()); socketWrapper.setWriteTimeout(getConnectionTimeout()); //3 交給poller處理 poller.register(channel, socketWrapper); return true; } ... // Tell to close the socket return false; }
3處,就是交給NioEndpoint內部的poller對象去進行處理。
public void register(final NioChannel socket, final NioSocketWrapper socketWrapper) { socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into. PollerEvent r = null; // 丟到poller的隊列裏,poller線程會輪旋該隊列 r = new PollerEvent(socket, OP_REGISTER); // 丟到隊列裏 addEvent(r); }
上面的addEvent值得一看。
private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>(); private void addEvent(PollerEvent event) { // 丟到隊列裏 events.offer(event); // 喚醒poller裏的selector,及時將該socket註冊到selector中 if (wakeupCounter.incrementAndGet() == 0) { selector.wakeup(); } }
到這裏,acceptor線程的邏輯就結束了,一個異步放隊列,完美收工。接下來,就是poller線程的工做了。
poller線程,要負責將該socket註冊到selector裏面去,而後還要負責該socket的io讀寫事件處理。
poller線程邏輯
public class Poller implements Runnable { private Selector selector; private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();
能夠看到,poller內部維護了一個selector,和一個隊列,隊列裏也說了,主要是要新註冊到selector的新socket。
既然丟到隊列了,那咱們看看何時去隊列取的呢?
@Override public void run() { // Loop until destroy() is called while (true) { boolean hasEvents = false; // 檢查events hasEvents = events(); } }
這裏咱們跟一下events()。
public boolean events() { boolean result = false; PollerEvent pe = null; for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) { result = true; pe.run(); ... } return result; }
這裏的
pe = events.poll()
就是去隊列拉取事件,拉取到了以後,就會賦值給pe,而後下面就調用了pe.run方法。
pe的類型是PollerEvent,咱們看看其run方法會幹啥?
@Override public void run() { if (interestOps == OP_REGISTER) { try { socket.getIOChannel().register(socket.getSocketWrapper().getPoller().getSelector(), SelectionKey.OP_READ, socket.getSocketWrapper()); } catch (Exception x) { log.error(sm.getString("endpoint.nio.registerFail"), x); } } }
這個方法難理解嗎,看着有點嚇人,其實就是把這個新的鏈接,向selector註冊,感興趣的io事件爲OP_READ。後續呢,這個鏈接的io讀寫,就全由本poller的selector包了。
咱們說了,poller是個線程,在其runnable實現裏,除了要處理上面的新鏈接註冊到selector這個事,還要負責io讀寫,這部分邏輯就是在:
Iterator<SelectionKey> iterator=selector.selectedKeys().iterator(); while (iterator != null && iterator.hasNext()) { SelectionKey sk = iterator.next(); NioSocketWrapper socketWrapper = sk.attachment(); processKey(sk, socketWrapper); }
最後一行的processKey,會調用以下邏輯,將工做甩鍋給http-nio-8080-exec-2@5076這類打雜的線程。
public boolean processSocket(SocketWrapperBase<S> socketWrapper,SocketEvent event, boolean dispatch) { Executor executor = getExecutor(); executor.execute(sc); return true; }
給個圖的話,大概就是以下的紅線流程部分了:
好了,到了課後思考時間了,咱們也說了,最終會交給http-nio-8080-exec-2@5076這類線程所在的線程池,那假設這些線程全都在sleep,會發生什麼呢?
下一篇,咱們繼續。