tomcat nio並不是真正的異步io,其實是io復用,可以說是非阻塞的,但不是真正的異步。
tomcat的NioEndpoint啟動的ServerSocket是阻塞的,Acceptor線程里邊阻塞從accept()獲取socket
socket是非阻塞的,每個socket的channel注冊到一個Poller線程上去,Poller會把這個channel包裝成一個PollerEvent,並放到events隊列里去。
默認有兩個Poller線程,每個Poller線程里邊有一個Selector,以及一個隊列:SynchronizedQueue<PollerEvent> events
btw:Acceptor線程和Poller線程都是NioEndpoint的內部類實現的)
Poller線程內部不斷循環events隊列 1、從里邊拿出PollerEvent去執行它的run方法,其中將PollerEvent的SocketChannel注冊到該poller的Selector中。
2、SocketChannel如果readable則執行processKey()
processKey()里邊調用processSocket(),processSocket()里邊把socket包裝成SocketProcessor交給線程池去執行。
線程池
在NioEndpoint的startInternal()方法里,也就是在啟動Poller和Acceptor線程之前把線程池建立起來createExecutor()
//創建線程池
public void createExecutor() {
internalExecutor = true;
TaskQueue taskqueue = new TaskQueue(); //任務隊列
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());//工程模式,告訴線程池怎么創建里邊的線程
//參數:核心線程數、最大線程數、60秒keepalive、任務隊列、以及用來創建線程用的線程工廠
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent( (ThreadPoolExecutor) executor);//任務隊列里邊存了一下線程池的引用,以后會用到
}
接下來就是研究TaskQueue、ThreadPoolExecutor了
看過來其實tomcat線程池還是基於jdk線程池的,只是在初始化的時候傳入了自定義的TaskQueue和ThreadFactory, 從而達到自定義線程池的目的。
最終目的是要分析好兩份jstact樣本:
異常,將近400個處於WAITING
"http-nio-8081-exec-385" #459 daemon prio=5 os_prio=0 tid=0x00007f4b6819a800 nid=0x674e waiting on condition [0x00007f4b2e977000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000006c8ddb088> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2083)
at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:85)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:31)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1073)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
正常樣本: 無請求時,tomcat線程池中idle線程10個,等待處理請求
"http-nio-8081-exec-1" #17 daemon prio=5 os_prio=0 tid=0x00007f3bf8aa9800 nid=0x15d0 waiting on condition [0x00007f3bd54d4000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000e41b3ad8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
to be continuted ...
上面的問題在於tomcat線程池的maxIdle配的太大了,比如coreSize=minIdle=10, maxIdle=400, maxActive=600,那么一旦高峰時候線程數來到了600,高峰退去,線程釋放到maxIdle就不會釋放了,maxIdle意思是最大運行空閑常駐多少線程。
這里邊還有個疑問,tomcat9的代碼里沒看到有關“maxIdle”的參數,就coreSize、maxSize、maxkeepalivetime這幾個參數,coresize是常駐,超過coresize小於maxsize的,maxkeepalivetime之內沒有使用則線程釋放,然后等的這段時間應該是poll(timeout)、對應的應該是TIMED_WAIT狀態。
這里是WAITING狀態,跟coresize時候常駐線程發現taskqueue里邊為空然后一直阻塞等待非空的情況一樣都是WAITING,然后org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:85)這句又指向線程是在做timeout(keepAliveTimeout默認60s)超時等待。
概況來說,筆者感覺從線程棧上來看是從任務隊列里拿任務時,線程阻塞了poll是超時阻塞,多線程並發從隊列里poll。(這里未必是隊列里為空,所以如果可以的話要對這個隊列加個監控、看一下隊列里任務數),是不是線程都是在從隊列里拿任務的時候都阻塞在鎖上了?由於某種性能上的原因,等待鎖的過程又很慢?
媽的,如果隊列里拿任務然后拿完就釋放鎖,這個過程應該很快,其他線程非公平的爭搶鎖,然后搶到以后就poll,暫時想不出隊列里有任務大家還都卡在poll上的情形。。。