tomcat的3個線程棧dump樣本分析

"朝花夕拾、不留遺憾。念念不忘,必有回響。"

通過通讀tomcat請求任務(wù)處理、tomcat線程池、TaskQueue、ReentrantLock以及AQS的源碼,以及對它們的功能和原理的了解,我們就可以分析tomcat線程dump里邊的蛛絲馬跡了。

另外關(guān)于樣本三,由于沒有實(shí)驗(yàn)重現(xiàn)問題進(jìn)而證明推斷,請多持懷疑態(tài)度。

樣本一

這個情況很簡單,本地啟動tomcat以后jstack就可以看到,沒太多好講的,我們快速過一遍。

"http-nio-8081-exec-27" #42 daemon prio=1 os_prio=-2 tid=0x000000001a549000 nid=0x4a70 waiting on condition [0x000000001dc0f000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x000000008144aab0> (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:107)
    at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:1)
    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:58)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

"http-nio-8081-exec-xx"這樣的線程有10個,是線程池coreSize的大小??瓷厦娴膸讉€關(guān)鍵地方,TaskQueue.take這是一個無超時阻塞,從任務(wù)隊(duì)列里出隊(duì),線程數(shù)少于核心線程Size的時候,用take方法出隊(duì)。

往上看此時take阻塞在ConditionObject.await其內(nèi)部是使用的LockSupport.park實(shí)現(xiàn)的阻塞。源碼里可以看到這個Condition是notEmpty,也就是當(dāng)前隊(duì)列是空的,核心線程都等待notEmpty這個條件滿足,然后競爭任務(wù)隊(duì)列里的takeLock(ReentrantLock非公平鎖,基于AQS排他模式實(shí)現(xiàn)),拿到鎖之后拿任務(wù)出隊(duì)然后執(zhí)行。

樣本二

這種情況發(fā)生在業(yè)務(wù)高峰過后,正常情況應(yīng)該會釋放部分空閑線程,進(jìn)入一個平衡點(diǎn),或者在業(yè)務(wù)極少的時候比如半夜會進(jìn)入樣本一狀態(tài)。

"http-nio-8081-exec-162" #177 daemon prio=1 os_prio=-2 tid=0x000000001bbec000 nid=0x2580 waiting on condition [0x00000000272df000]
   java.lang.Thread.State: TIMED_WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x000000008144aab0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
    at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
    at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:89)
    at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:1)
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1066)
    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:58)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

簡單提兩句,LinkedBlockingQueue.poll我們知道線程數(shù)大于coreSize的時候,任務(wù)隊(duì)列出隊(duì)會用poll而不是take,接下來ConditionObject.awaitNanos我們?nèi)タ匆幌耇askQueue和LinkedBlockingQueue的源碼也會知道這還是阻塞在notEmpty條件上、只不過這次是超時阻塞,也就是說線程池設(shè)置的keepalive最大空閑時間之內(nèi)沒有被使用的話,該線程最終會走向終結(jié)。至于有時候?yàn)槭裁催@類線程要經(jīng)過很久的一個緩慢釋放過程,我們在tomcat的worker線程的空閑判定與釋放 - 簡書 (jianshu.com) 里做了較為詳細(xì)的分析。

樣本三

373個這樣處于WAITING的http-nio-8081-exec線程:

"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)

一個處于RUNNABLE的線程:

"http-nio-8081-exec-386" #461 daemon prio=5 os_prio=0 tid=0x00007f4b74511000 nid=0x683f runnable [0x00007f4b2e773000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)

我們這次重點(diǎn)分析一下這個樣本三。當(dāng)時的情形,筆者回憶也是個業(yè)務(wù)高峰,這個springboot tomcat應(yīng)該是進(jìn)程還在、端口也通,但是請求基本不響應(yīng)了的一個“假死”狀態(tài)。

樣本三和樣本二是從awaitNanos之后開始不同的。awaitNanos(long nanosTimeout)這個方法是在LinkedBlockingQueue里邊poll(timeout)的時候先拿鎖,然后等待在notEmpty這個Condition這個條件上,調(diào)用notEmpty.awaitNanos(nanos),notEmpty是隊(duì)列的ReentrantLock takeLock這個重入鎖上的Condition。

LinkedBlockingQueue的poll(timeout)方法:

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    final E x;
    final int c;
    long nanos = unit.toNanos(timeout);
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();   //先拿鎖
    try {
        while (count.get() == 0) { //隊(duì)列為空
            if (nanos <= 0L)
                return null;  //如果等了timeout時間隊(duì)列還是空的,就返回null
            nanos = notEmpty.awaitNanos(nanos); //阻塞等待非空條件、并釋放鎖
        }
        x = dequeue();  //隊(duì)列有任務(wù)了,出隊(duì)
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();  //出隊(duì)了一個任務(wù)之后隊(duì)列還不是空的,那么喚醒等在notEmpty上的其他線程都去爭搶鎖并從await繼續(xù)執(zhí)行。
    } finally {
        takeLock.unlock();  //釋放鎖
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

我們接著去awaitNanos里邊看看:

public final long awaitNanos(long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // We don't check for nanosTimeout <= 0L here, to allow
    // awaitNanos(0) as a way to "yield the lock".
    final long deadline = System.nanoTime() + nanosTimeout;
    long initialNanos = nanosTimeout;
    Node node = addConditionWaiter();   //new Node(Node.CONDITION)并添加到條件隊(duì)列隊(duì)尾
    int savedState = fullyRelease(node);//釋放獨(dú)占鎖,unpark喚醒后繼節(jié)點(diǎn)搶鎖
    int interruptMode = 0;
    
    //在while里邊說明仍然需要在條件隊(duì)列里
    //比如假如隊(duì)列里來任務(wù)了,會對notEmpty進(jìn)行signal,這樣Condition隊(duì)列里的節(jié)點(diǎn)會transferForSignal到同步隊(duì)列去
    //即Condition.await 本質(zhì)是把自己丟入條件隊(duì)列,然后等待signal喚醒、如果被喚醒了就再次進(jìn)入同步隊(duì)列;同時釋放鎖、使得同步隊(duì)列里的后續(xù)節(jié)點(diǎn)可以搶鎖去看條件滿足與否。
    while (!isOnSyncQueue(node)) {//node在隊(duì)列里,并且不是第一個且waitStatus == Node.CONDITION,則返回true; 
        if (nanosTimeout <= 0L) {
            transferAfterCancelledWait(node);
            break;
        }
        if (nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
            LockSupport.parkNanos(this, nanosTimeout);   //樣本二, 還是在條件隊(duì)列里,等待
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        nanosTimeout = deadline - System.nanoTime();
    }
    
    //走到這說明已經(jīng)(滿足條件)在同步隊(duì)列等待重新獲取鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE) //樣本三
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    long remaining = deadline - System.nanoTime(); // avoid overflow
    return (remaining <= initialNanos) ? remaining : Long.MIN_VALUE;
}

接下來我們先跳躍一下,去看看任務(wù)隊(duì)列的入隊(duì)邏輯,從LinkedBlockingQueue的offer方法開始:

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity)
        return false;   //達(dá)到最大隊(duì)列容量了,返回false
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); //先拿鎖
    try {
        if (count.get() < capacity) {
            enqueue(node);  //入隊(duì)
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal(); //隊(duì)列沒滿,signal喚醒等待notFull條件的線程
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0) //c==0說明之前隊(duì)列是空的,現(xiàn)在入隊(duì)了,那么就非空了。也只有這時候才需要signal非空、隊(duì)列本就不為空的話不需要signal非空
        signalNotEmpty(); //隊(duì)列非空,signal喚醒等待notEmpty條件的線程
    return c >= 0;
}

上面的signalNotEmpty()最終會執(zhí)行到transferForSignal(Node node)方法里,這個就是把線程(對應(yīng)的Node)從條件隊(duì)列轉(zhuǎn)移到同步隊(duì)列的操作。

分析這2個方法要了解AQS的原理,從poll里邊的count.get() == 0線程進(jìn)來以后,進(jìn)入了條件隊(duì)列排隊(duì),然后釋放了takeLock,就鉆到while (!isOnSyncQueue(node))這個循環(huán)里了、超時之前不斷判斷自己有沒有在同步隊(duì)列里,也就是等任務(wù)隊(duì)列有入隊(duì)了,非空了,條件隊(duì)列里邊的線程都會被移到同步隊(duì)列里頭排隊(duì)重新獲取takeLock然后干活。

樣本三的線程棧上可以看出來,線程跳出了while循環(huán),來到了if (acquireQueued(node, savedState) && interruptMode != THROW_IE),也就是說,隊(duì)列里有入隊(duì)了而且應(yīng)該是至少300多個,這些原來等待在條件隊(duì)列的線程被轉(zhuǎn)移到了同步隊(duì)列等待獲取takeLock干活了!我們繼續(xù),進(jìn)入acquireQueued:

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            //要么不是頭節(jié)點(diǎn),要么tryAcquire獲取鎖失敗了;
            if (shouldParkAfterFailedAcquire(p, node)) //前邊節(jié)點(diǎn)狀態(tài)是SIGNAL則park
                interrupted |= parkAndCheckInterrupt();  //park掛起當(dāng)前線程,等前邊節(jié)點(diǎn)unpark; 那400個線程都park在這了!
        } //跳出循環(huán)的條件是前置節(jié)點(diǎn)為頭節(jié)點(diǎn)且搶到鎖。非公平鎖,當(dāng)前持有鎖的線程也會跟后繼節(jié)點(diǎn)去搶鎖。
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

到此,我們大致知道了流程:notEmpty條件不滿足就先阻塞在takeLock.condition上,滿足了以后,由于是非公平鎖就先tryAcquire拿一次鎖、如果成則去拿隊(duì)列任務(wù)執(zhí)行,沒拿到就進(jìn)同步隊(duì)列排隊(duì)拿鎖、除了第一個外其余park等前序喚醒。
從線程棧上看這300+個線程就屬于是在AQS同步隊(duì)列里park等待前序節(jié)點(diǎn)喚醒再去拿鎖的這種場景。

這是一個線程池饑餓問題嗎 ?還是一個性能瓶頸問題?

筆者設(shè)想了如下兩個可能出現(xiàn)上述情況的場景:

1、非公平鎖模式下,300+個線程,某個386號線程由于某種原因,每次都搶到鎖。tomcat任務(wù)隊(duì)列是jdk LinkedBlockingQueue的子類,出隊(duì)鎖takeLock就是非公平鎖。386號是當(dāng)前持有鎖的線程,poll完之后takeLock.unlock()釋放鎖并unpark同步隊(duì)列中自己后繼節(jié)點(diǎn)了,自己接下來去執(zhí)行task任務(wù)了。然后跟后繼節(jié)點(diǎn)是可以一起來poll -> tryAcquire來搶鎖的。然后又是它搶到了。

雜(jvm線程棧分析、jdkbug引發(fā)socketRead0阻塞、jvm調(diào)優(yōu)等) - 肥兔子愛豆畜子 - 博客園 (cnblogs.com)

可能的情況:386線程一直處于RUNNABLE狀態(tài),系統(tǒng)將調(diào)度優(yōu)先給這個線程,其他同步隊(duì)列的線程得不到時間片搶不到鎖。然后386線程由于上述jdk bug又執(zhí)行不完一直處于這個狀態(tài)。所以整個進(jìn)程假死。

2、任務(wù)隊(duì)列里任務(wù)一下來了非常之多的任務(wù),然后之前條件隊(duì)列里的線程都滿足了notEmpty條件去搶鎖然后拿任務(wù),然后也是按照同步隊(duì)列里的順序一個接著一個的喚醒去拿鎖然后拿任務(wù)執(zhí)行請求業(yè)務(wù)邏輯。剛好jstack的時候這些在同步隊(duì)列里的且還沒被喚醒的park線程被采集到線程棧了。
請求任務(wù)源源不斷的的入隊(duì)(TaskQueue),worker線程就會一直滿足notEmpty的Condition而進(jìn)入到AQS同步隊(duì)列里、排隊(duì)、park,這樣只要入隊(duì)的速度大于出隊(duì)的速度(這里指AQS同步隊(duì)列),那么就會產(chǎn)生上面300+個線程排隊(duì)獲取TaskQueue的ReentrantLock$NonfairSync鎖的現(xiàn)象了。

這就其實(shí)是所謂的性能瓶頸了,任務(wù)隊(duì)列里產(chǎn)生了大量積壓(所以tomcat新請求來了沒響應(yīng)假死、實(shí)際上是進(jìn)到任務(wù)隊(duì)列排隊(duì)去了,而且隊(duì)非常之長),后面的工作線程AQS排隊(duì)去拿takeLock然后一個個的執(zhí)行隊(duì)列里的任務(wù)卻怎么也執(zhí)行不完,隊(duì)列太長了。

如果是2這種情況,那不應(yīng)該是從awaitNanos進(jìn)去,后續(xù)一直worker線程忙于處理的話,應(yīng)該是count.get() != 0,然后直接出隊(duì),就是park也應(yīng)該是從takeLock.lockInterruptibly()進(jìn)去走acquireQueued。

所以從線程棧上來看,是一種線程先空閑從條件隊(duì)列瞬間移動到同步隊(duì)列、然后300+大部分沒搶到鎖的線程進(jìn)入park,然后搶到鎖的386線程一直占用CPU時間片(雖然筆者還不了解更底層的線程調(diào)度原理來證明這種線程饑餓的情況),其他線程沒機(jī)會去搶鎖運(yùn)行這樣一個瞬時里發(fā)生的事情。

所以,筆者目前傾向是一個jdk bug導(dǎo)致的一個線程長期占用cpu,其他大量線程饑餓park阻塞的情形。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容