public static enum Thread.Stateextends Enum<Thread.State>線(xiàn)程狀態(tài)墩崩。線(xiàn)程可以處于下列狀態(tài)之一:
線(xiàn)程的生命周期包括哪幾個(gè)階段
面試官:您知道線(xiàn)程的生命周期包括哪幾個(gè)階段族淮?
應(yīng)聘者:
線(xiàn)程的生命周期包含 5 個(gè)階段,包括:新建愿卒、就緒、運(yùn)行羞酗、阻塞状土、銷(xiāo)毀无蜂。
新建:就是剛使用 new 方法,new 出來(lái)的線(xiàn)程蒙谓;
就緒:就是調(diào)用的線(xiàn)程的 start() 方法后斥季,這時(shí)候線(xiàn)程處于等待 CPU 分配資源階段,誰(shuí)先搶的 CPU 資源累驮,誰(shuí)開(kāi)始執(zhí)行;
運(yùn)行:當(dāng)就緒的線(xiàn)程被調(diào)度并獲得 CPU 資源時(shí)泻肯,便進(jìn)入運(yùn)行狀態(tài)渊迁,run 方法定義了線(xiàn)程的操作和功能;
阻塞:在運(yùn)行狀態(tài)的時(shí)候,可能因?yàn)槟承┰驅(qū)е逻\(yùn)行狀態(tài)的線(xiàn)程變成了阻塞狀態(tài)灶挟,比如 sleep()琉朽、wait() 之后線(xiàn)程就處于了阻塞狀態(tài),這個(gè)時(shí)候需要其他機(jī)制將處于阻塞狀態(tài)的線(xiàn)程喚醒稚铣,比如調(diào)用 notify 或者 notifyAll() 方法箱叁。喚醒的線(xiàn)程不會(huì)立刻執(zhí)行 run 方法,它們要再次等待 CPU 分配資源進(jìn)入運(yùn)行狀態(tài);
銷(xiāo)毀:如果線(xiàn)程正常執(zhí)行完畢后或線(xiàn)程被提前強(qiáng)制性的終止或出現(xiàn)異常導(dǎo)致結(jié)束惕医,那么線(xiàn)程就要被銷(xiāo)毀耕漱,釋放資源;
完整的生命周期圖如下:
新建狀態(tài)
我們來(lái)看下面一段代碼:
Thread t1 = new Thread();
這里的創(chuàng)建,僅僅是在 JAVA 的這種編程語(yǔ)言層面被創(chuàng)建抬伺,而在操作系統(tǒng)層面螟够,真正的線(xiàn)程還沒(méi)有被創(chuàng)建。只有當(dāng)我們調(diào)用了 start() 方法之后峡钓,該線(xiàn)程才會(huì)被創(chuàng)建出來(lái)妓笙,進(jìn)入 Runnable 狀態(tài)。只有當(dāng)我們調(diào)用了 start() 方法之后能岩,該線(xiàn)程才會(huì)被創(chuàng)建出來(lái)
就緒狀態(tài)
調(diào)用 start() 方法后寞宫,JVM 進(jìn)程會(huì)去創(chuàng)建一個(gè)新的線(xiàn)程,而此線(xiàn)程不會(huì)馬上被 CPU 調(diào)度運(yùn)行拉鹃,進(jìn)入 Running 狀態(tài)辈赋,這里會(huì)有一個(gè)中間狀態(tài),就是 Runnable 狀態(tài)膏燕,你可以理解為等待被 CPU 調(diào)度的狀態(tài)
t1.start()
用一張圖表示如下:
那么處于 Runnable 狀態(tài)的線(xiàn)程能發(fā)生哪些狀態(tài)轉(zhuǎn)變钥屈?
Runnable 狀態(tài)的線(xiàn)程無(wú)法直接進(jìn)入 Blocked 狀態(tài)和 Terminated 狀態(tài)的。只有處在 Running 狀態(tài)的線(xiàn)程坝辫,換句話(huà)說(shuō)篷就,只有獲得 CPU 調(diào)度執(zhí)行權(quán)的線(xiàn)程才有資格進(jìn)入 Blocked 狀態(tài)和 Terminated 狀態(tài),Runnable 狀態(tài)的線(xiàn)程要么能被轉(zhuǎn)換成 Running 狀態(tài)阀溶,要么被意外終止。
運(yùn)行狀態(tài)
當(dāng) CPU 調(diào)度發(fā)生鸦泳,并從任務(wù)隊(duì)列中選中了某個(gè) Runnable 線(xiàn)程時(shí)银锻,該線(xiàn)程會(huì)進(jìn)入 Running 執(zhí)行狀態(tài),并且開(kāi)始調(diào)用 run() 方法中邏輯代碼做鹰。
那么處于 Running 狀態(tài)的線(xiàn)程能發(fā)生哪些狀態(tài)轉(zhuǎn)變击纬?
被轉(zhuǎn)換成 Terminated 狀態(tài),比如調(diào)用 stop() 方法;
被轉(zhuǎn)換成 Blocked 狀態(tài)钾麸,比如調(diào)用了 sleep, wait 方法被加入 waitSet 中更振;
被轉(zhuǎn)換成 Blocked 狀態(tài)炕桨,如進(jìn)行 IO 阻塞操作,如查詢(xún)數(shù)據(jù)庫(kù)進(jìn)入阻塞狀態(tài)肯腕;
被轉(zhuǎn)換成 Blocked 狀態(tài)献宫,比如獲取某個(gè)鎖的釋放,而被加入該鎖的阻塞隊(duì)列中实撒;
該線(xiàn)程的時(shí)間片用完姊途,CPU 再次調(diào)度,進(jìn)入 Runnable 狀態(tài)知态;
線(xiàn)程主動(dòng)調(diào)用 yield 方法捷兰,讓出 CPU 資源,進(jìn)入 Runnable 狀態(tài)
阻塞狀態(tài)
Blocked 狀態(tài)的線(xiàn)程能夠發(fā)生哪些狀態(tài)改變负敏?
被轉(zhuǎn)換成 Terminated 狀態(tài)贡茅,比如調(diào)用 stop() 方法,或者是 JVM 意外 Crash;
被轉(zhuǎn)換成 Runnable 狀態(tài)其做,阻塞時(shí)間結(jié)束顶考,比如讀取到了數(shù)據(jù)庫(kù)的數(shù)據(jù)后;
完成了指定時(shí)間的休眠庶柿,進(jìn)入到 Runnable 狀態(tài)村怪;
正在 wait 中的線(xiàn)程,被其他線(xiàn)程調(diào)用 notify/notifyAll 方法喚醒浮庐,進(jìn)入到 Runnable 狀態(tài)甚负;
線(xiàn)程獲取到了想要的鎖資源,進(jìn)入 Runnable 狀態(tài)审残;
線(xiàn)程在阻塞狀態(tài)下被打斷梭域,如其他線(xiàn)程調(diào)用了 interrupt 方法,進(jìn)入到 Runnable 狀態(tài)搅轿;
終止?fàn)顟B(tài)
一旦線(xiàn)程進(jìn)入了 Terminated 狀態(tài)病涨,就意味著這個(gè)線(xiàn)程生命的終結(jié),哪些情況下璧坟,線(xiàn)程會(huì)進(jìn)入到 Terminated 狀態(tài)呢既穆?
線(xiàn)程正常運(yùn)行結(jié)束,生命周期結(jié)束雀鹃;
線(xiàn)程運(yùn)行過(guò)程中出現(xiàn)意外錯(cuò)誤幻工;
JVM 異常結(jié)束,所有的線(xiàn)程生命周期均被結(jié)束黎茎。
synchronized底層實(shí)現(xiàn)
synchronized不論是修飾方法還是代碼塊囊颅,都是通過(guò)持有修飾對(duì)象的鎖來(lái)實(shí)現(xiàn)同步,那么synchronized鎖對(duì)象是存在哪里的呢?答案是存在鎖對(duì)象的對(duì)象頭Mark Word踢代,來(lái)看一下Mark Word存儲(chǔ)了哪些內(nèi)容盲憎?
由于對(duì)象頭的信息是與對(duì)象自身定義的數(shù)據(jù)沒(méi)有關(guān)系的額外存儲(chǔ)成本,因此考慮到JVM的空間效率胳挎,Mark Word 被設(shè)計(jì)成為一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)饼疙,以便存儲(chǔ)更多有效的數(shù)據(jù),它會(huì)根據(jù)對(duì)象本身的狀態(tài)復(fù)用自己的存儲(chǔ)空間串远,也就是說(shuō)宏多,Mark Word會(huì)隨著程序的運(yùn)行發(fā)生變化,變化狀態(tài)如下 (32位虛擬機(jī)):
其中輕量級(jí)鎖和偏向鎖是Java 6 對(duì) synchronized 鎖進(jìn)行優(yōu)化后新增加的澡罚,稍后我們會(huì)簡(jiǎn)要分析伸但。這里我們主要分析一下重量級(jí)鎖也就是通常說(shuō)synchronized的對(duì)象鎖,鎖標(biāo)識(shí)位為10留搔,其中指針指向的是monitor對(duì)象(也稱(chēng)為管程或監(jiān)視器鎖)的起始地址更胖。每個(gè)對(duì)象都存在著一個(gè) monitor 與之關(guān)聯(lián)。在Java虛擬機(jī)(HotSpot)中隔显,monitor是由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機(jī)源碼ObjectMonitor.hpp文件括眠,C++實(shí)現(xiàn)的)彪标,省略部分屬性
ObjectMonitor() {
_count = 0; //記錄數(shù)
_recursions = 0; //鎖的重入次數(shù)
_owner = NULL; //指向持有ObjectMonitor對(duì)象的線(xiàn)程
_WaitSet = NULL; //調(diào)用wait后,線(xiàn)程會(huì)被加入到_WaitSet
_EntryList = NULL ; //等待獲取鎖的線(xiàn)程掷豺,會(huì)被加入到該列表
}
結(jié)合線(xiàn)程狀態(tài)解釋一下執(zhí)行過(guò)程捞烟。(狀態(tài)裝換參考自《深入理解Java虛擬機(jī)》)
新建(New),新建后尚未啟動(dòng)的線(xiàn)程
運(yùn)行(Runable)当船,Runnable包括了操作系統(tǒng)線(xiàn)程狀態(tài)中的Running和Ready
無(wú)限期等待(Waiting)题画,不會(huì)被分配CPU執(zhí)行時(shí)間,要等待被其他線(xiàn)程顯式的喚醒德频。例如調(diào)用沒(méi)有設(shè)置Timeout參數(shù)的Object.wait()方法
限期等待(Timed Waiting)苍息,不會(huì)被分配CPU執(zhí)行時(shí)間,不過(guò)無(wú)需等待其他線(xiàn)程顯示的喚醒壹置,在一定時(shí)間之后會(huì)由系統(tǒng)自動(dòng)喚醒竞思。例如調(diào)用Thread.sleep()方法
阻塞(Blocked),線(xiàn)程被阻塞了钞护,“阻塞狀態(tài)”與“等待狀態(tài)”的區(qū)別是:“阻塞狀態(tài)”在等待獲取著一個(gè)排他鎖盖喷,這個(gè)事件將在另外一個(gè)線(xiàn)程放棄這個(gè)鎖的時(shí)候發(fā)生,而“等待狀態(tài)”則是在等待一段時(shí)間患亿,或者喚醒動(dòng)作的發(fā)生传蹈。在程序等待進(jìn)入同步區(qū)域的時(shí)候,線(xiàn)程將進(jìn)入這種狀態(tài)
結(jié)束(Terminated):線(xiàn)程結(jié)束執(zhí)行
對(duì)于一個(gè)synchronized修飾的方法(代碼塊)來(lái)說(shuō):
當(dāng)多個(gè)線(xiàn)程同時(shí)訪問(wèn)該方法步藕,那么這些線(xiàn)程會(huì)先被放進(jìn)_EntryList隊(duì)列惦界,此時(shí)線(xiàn)程處于blocked狀態(tài)
當(dāng)一個(gè)線(xiàn)程獲取到了對(duì)象的monitor后,那么就可以進(jìn)入running狀態(tài)咙冗,執(zhí)行方法沾歪,此時(shí),ObjectMonitor對(duì)象的/_owner指向當(dāng)前線(xiàn)程雾消,_count加1表示當(dāng)前對(duì)象鎖被一個(gè)線(xiàn)程獲取
當(dāng)running狀態(tài)的線(xiàn)程調(diào)用wait()方法灾搏,那么當(dāng)前線(xiàn)程釋放monitor對(duì)象本砰,進(jìn)入waiting狀態(tài)缚态,ObjectMonitor對(duì)象的/_owner變?yōu)閚ull,_count減1染厅,同時(shí)線(xiàn)程進(jìn)入_WaitSet隊(duì)列桑腮,直到有線(xiàn)程調(diào)用notify()方法喚醒該線(xiàn)程泉哈,則該線(xiàn)程進(jìn)入_EntryList隊(duì)列,競(jìng)爭(zhēng)到鎖再進(jìn)入_Owner區(qū)
如果當(dāng)前線(xiàn)程執(zhí)行完畢破讨,那么也釋放monitor對(duì)象丛晦,ObjectMonitor對(duì)象的/_owner變?yōu)閚ull,_count減1
由此看來(lái)提陶,monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中(存儲(chǔ)的是指針)烫沙,synchronized鎖便是通過(guò)這種方式獲取鎖的,也是為什么Java中任意對(duì)象可以作為鎖的原因隙笆,同時(shí)也是notify/notifyAll/wait等方法存在于頂級(jí)對(duì)象Object中的原因
Java.Thread.State狀態(tài)
1.NEW
至今尚未啟動(dòng)的線(xiàn)程的狀態(tài)锌蓄。
2.RUNNABLE
可運(yùn)行線(xiàn)程的線(xiàn)程狀態(tài)。處于可運(yùn)行狀態(tài)的某一線(xiàn)程正在 Java 虛擬機(jī)中運(yùn)行仲器,但它可能正在等待操作系統(tǒng)中的其他資源煤率,比如處理器。
3.BLOCKED
受阻塞并且正在等待監(jiān)視器鎖的某一線(xiàn)程的線(xiàn)程狀態(tài)乏冀。處于受阻塞狀態(tài)的某一線(xiàn)程正在等待監(jiān)視器鎖蝶糯,以便進(jìn)入一個(gè)同步的塊/方法,或者在調(diào)用 Object.wait 之后再次進(jìn)入同步的塊/方法辆沦。
4.WAITING
某一等待線(xiàn)程的線(xiàn)程狀態(tài)昼捍。某一線(xiàn)程因?yàn)檎{(diào)用下列方法之一而處于等待狀態(tài):
不帶超時(shí)值的 Object.wait
不帶超時(shí)值的 Thread.join
LockSupport.park
處于等待狀態(tài)的線(xiàn)程正等待另一個(gè)線(xiàn)程,以執(zhí)行特定操作肢扯。 例如妒茬,已經(jīng)在某一對(duì)象上調(diào)用了 Object.wait() 的線(xiàn)程正等待另一個(gè)線(xiàn)程,以便在該對(duì)象上調(diào)用 Object.notify() 或 Object.notifyAll()蔚晨。已經(jīng)調(diào)用了 Thread.join() 的線(xiàn)程正在等待指定線(xiàn)程終止乍钻。
5.TIMED_WAITING具有指定等待時(shí)間的某一等待線(xiàn)程的線(xiàn)程狀態(tài)肛循。某一線(xiàn)程因?yàn)檎{(diào)用以下帶有指定正等待時(shí)間的方法之一而處于定時(shí)等待狀態(tài):
Thread.sleep
帶有超時(shí)值的 Object.wait
帶有超時(shí)值的 Thread.join
LockSupport.parkNanos
LockSupport.parkUntil
6.TERMINATED
已終止線(xiàn)程的線(xiàn)程狀態(tài)。線(xiàn)程已經(jīng)結(jié)束執(zhí)行银择。
線(xiàn)程狀態(tài)與CPU占用的關(guān)系
參考原文
原文
背景
剛剛過(guò)去的雙十一, 公司訂單量又翻了一倍. 就在老板坐在辦公室里面偷偷笑的同時(shí),坐在工位上的我們卻是一直瑟瑟發(fā)抖. 面對(duì)zabbix里面時(shí)不時(shí)蹦出來(lái)的一條條CPU告警,默默地祈禱著不要出問(wèn)題.
當(dāng)然, 祈禱是解決不了問(wèn)題的, 即使是開(kāi)過(guò)光的服務(wù)器也不行. CPU告警了, 還得老老實(shí)實(shí)地去看為啥CPU飚起來(lái)了.
接下來(lái)就是CPU排查三部曲
1. top -Hp $pid 找到最耗CPU的線(xiàn)程. 2. 將最耗CPU的線(xiàn)程ID轉(zhuǎn)成16進(jìn)制 3. 打印jstack, 到j(luò)stack里面查這個(gè)線(xiàn)程在干嘛 復(fù)制代碼
當(dāng)然 如果你線(xiàn)上環(huán)境有裝arthas等工具的話(huà), 直接thread -n就可以打印出最耗cpu的n個(gè)線(xiàn)程的堆棧,三個(gè)步驟一起幫你做了.
最后找到最耗cpu的線(xiàn)程堆棧如下:
"operate-api-1-thread-6" #1522 prio=5 os\_prio=0 tid=0x00007f4b7006f800 nid=0x1b67c waiting on condition \[0x00007f4ac8c4a000\] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000006c10828c8> (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.acquire(AbstractQueuedSynchronizer.java:1199) at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209) at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285) at ch.qos.logback.core.OutputStreamAppender.subAppend(OutputStreamAppender.java:210) at ch.qos.logback.core.rolling.RollingFileAppender.subAppend(RollingFileAppender.java:235) at ch.qos.logback.core.OutputStreamAppender.append(OutputStreamAppender.java:100) at ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend(UnsynchronizedAppenderBase.java:84) at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:51) at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:270) at ch.qos.logback.classic.Logger.callAppenders(Logger.java:257) at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:421) at ch.qos.logback.classic.Logger.filterAndLog\_0_Or3Plus(Logger.java:383) at ch.qos.logback.classic.Logger.info(Logger.java:579) ... 復(fù)制代碼
值得一提的是, 類(lèi)似的線(xiàn)程還有800多個(gè)... 只是部分沒(méi)有消耗CPU而已
問(wèn)題
很明顯, 這是因?yàn)閘ogback打印日志太多了造成的(此時(shí)應(yīng)有一個(gè)尷尬而不失禮貌的假笑).
當(dāng)大家都紛紛轉(zhuǎn)向討論接下來(lái)如何優(yōu)化logback和打日志的時(shí)候. 我卻眉頭一皺, 覺(jué)得事情并沒(méi)有那么簡(jiǎn)單:
這個(gè)線(xiàn)程不是被LockSupport.park掛起了, 處于WAITING狀態(tài)嗎? 被掛起即代表放棄占用CPU了, 那為啥還會(huì)消耗CPU呢?
來(lái)看一下LockSupport.park的注釋, 明確提到park的線(xiàn)程不會(huì)再被CPU調(diào)度了的:
/** * Disables the current thread for thread scheduling purposes unless the * permit is available. * * <p>If the permit is available then it is consumed and the call * returns immediately; otherwise the current thread becomes disabled * for thread scheduling purposes and lies dormant until one of three * things happens: * */ public static void park() { UNSAFE.park(false, 0L); } 復(fù)制代碼
實(shí)驗(yàn)見(jiàn)真知
帶著這個(gè)疑問(wèn), 我在stackoverflow搜索了一波, 發(fā)現(xiàn)還有不少人有這個(gè)疑問(wèn)
上面好幾個(gè)問(wèn)題內(nèi)容有點(diǎn)多, 我也懶得翻譯了, 直接總結(jié)結(jié)論:
1. 處于waittig和blocked狀態(tài)的線(xiàn)程都不會(huì)消耗CPU 2. 線(xiàn)程頻繁地掛起和喚醒需要消耗CPU, 而且代價(jià)頗大 復(fù)制代碼
但這是別人的結(jié)論, 到底是不是這樣的呢. 下面我們結(jié)合visualvm來(lái)做一下實(shí)驗(yàn).
有問(wèn)題的代碼
首先來(lái)看一段肯定會(huì)消耗100%CPU的代碼:
package com.test; public class TestCpu { public static void main(String\[\] args) { while(true){ } } } 復(fù)制代碼
visualvm顯示CPU確實(shí)消耗了1個(gè)核, main線(xiàn)程也是占用了100%的CPU:
[圖片上傳失敗...(image-897af1-1636004498399)]
[圖片上傳失敗...(image-6a087b-1636004498400)]
被park的線(xiàn)程
然后來(lái)看一下park的線(xiàn)程是否會(huì)消耗cpu
代碼:
import java.util.concurrent.locks.LockSupport; public class TestCpu { public static void main(String\[\] args) { while(true){ LockSupport.park(); } } } 復(fù)制代碼
visualvm顯示一切波瀾不驚,CPU毫無(wú)壓力 :
[圖片上傳失敗...(image-56c905-1636004498400)]
[圖片上傳失敗...(image-30afdc-1636004498400)]
發(fā)生死鎖的線(xiàn)程
再來(lái)看看blocked的線(xiàn)程是否消耗CPU. 而且我們這次玩大一點(diǎn), 看看出現(xiàn)了死鎖的話(huà),會(huì)不會(huì)造成CPU飆高.(死鎖就是兩個(gè)線(xiàn)程互相block對(duì)方)
死鎖代碼如下:
package com.test; public class DeadLock { static Object lock1 = new Object(); static Object lock2 = new Object(); public static class Task1 implements Runnable { @Override public void run() { synchronized (lock1) { System.out.println(Thread.currentThread().getName() + " 獲得了第一把鎖!!"); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println(Thread.currentThread().getName() + " 獲得了第二把鎖!!"); } } } } public static class Task2 implements Runnable { @Override public void run() { synchronized (lock2) { System.out.println(Thread.currentThread().getName() + " 獲得了第二把鎖!!"); synchronized (lock1) { System.out.println(Thread.currentThread().getName() + " 獲得了第一把鎖!!"); } } } } public static void main(String\[\] args) throws InterruptedException { Thread thread1 = new Thread(new Task1(), "task-1"); Thread thread2 = new Thread(new Task2(), "task-2"); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(Thread.currentThread().getName() + " 執(zhí)行結(jié)束!"); } } 復(fù)制代碼
[圖片上傳失敗...(image-46a180-1636004498400)]
[圖片上傳失敗...(image-7c9652-1636004498400)]
[圖片上傳失敗...(image-170b8e-1636004498400)]
也是可以看到雖然visualVm能檢測(cè)到了死鎖, 但是整個(gè)JVM消耗的CPU并沒(méi)有什么大的起伏的. 也就是說(shuō)就算是出現(xiàn)了死鎖,理論上也不會(huì)影響到系統(tǒng)CPU.
當(dāng)然,雖然死鎖不會(huì)影響到CPU, 但是一個(gè)系統(tǒng)的資源并不只有CPU這一種, 死鎖的出現(xiàn)還是有可能導(dǎo)致某種資源的耗盡,而最終導(dǎo)致服務(wù)不可用, 所以死鎖還是要避免的.
頻繁切換線(xiàn)程上下文的場(chǎng)景
最后, 來(lái)看看大量線(xiàn)程切換是否會(huì)影響到JVM的CPU.
我們先生成數(shù)2000個(gè)線(xiàn)程, 利用jdk提供的LockSupport.park()不斷掛起這些線(xiàn)程. 再使用LockSupport.unpark(t)不斷地喚醒這些線(xiàn)程. 喚醒之后又立馬掛起. 以此達(dá)到不斷切換線(xiàn)程的目的.
代碼如下:
package com.test; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.locks.LockSupport; public class TestCpu { public static void main(String\[\] args) { int threadCount = 2000; if(args.length > 0){ threadCount = Integer.parseInt(args\[0\].trim()); } final List<Thread> list = new ArrayList<>(threadCount); // 啟動(dòng)threadCount個(gè)線(xiàn)程, 不斷地park/unpark, 來(lái)表示線(xiàn)程間的切換 for(int i =0; i<threadCount; i++){ Thread thread = new Thread(()->{ while(true){ LockSupport.park(); System.out.println(Thread.currentThread() +" was unpark"); } }); thread.setName("cpuThread" + i); list.add(thread); thread.start(); } // 隨機(jī)地unpark某個(gè)線(xiàn)程 while(true){ int i = new Random().nextInt(threadCount); Thread t = list.get(i); if(t != null){ LockSupport.unpark(t); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }finally { } } } } 復(fù)制代碼
再觀察visualVm, 發(fā)現(xiàn)整個(gè)JVM的CPU的確開(kāi)始升高了, 但是具體到線(xiàn)程級(jí)別, 會(huì)發(fā)現(xiàn)每個(gè)線(xiàn)程都基本不耗CPU. 說(shuō)明CPU不是這些線(xiàn)程本身消耗的. 而是系統(tǒng)在進(jìn)行線(xiàn)程上下文切換時(shí)消耗的:
jvm的cpu情況:
[圖片上傳失敗...(image-eba20d-1636004498400)]
每個(gè)線(xiàn)程的占用cpu情況:
[圖片上傳失敗...(image-f38e54-1636004498400)]
分析和總結(jié)
再回到我們文章開(kāi)頭的線(xiàn)程堆棧(占用了15%的CPU):
"operate-api-1-thread-6" #1522 prio=5 os\_prio=0 tid=0x00007f4b7006f800 nid=0x1b67c waiting on condition \[0x00007f4ac8c4a000\] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000006c10828c8> (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.acquire(AbstractQueuedSynchronizer.java:1199) at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209) at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285) at ch.qos.logback.core.OutputStreamAppender.subAppend(OutputStreamAppender.java:210) at ch.qos.logback.core.rolling.RollingFileAppender.subAppend(RollingFileAppender.java:235) at ch.qos.logback.core.OutputStreamAppender.append(OutputStreamAppender.java:100) at ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend(UnsynchronizedAppenderBase.java:84) at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:51) at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:270) at ch.qos.logback.classic.Logger.callAppenders(Logger.java:257) at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:421) at ch.qos.logback.classic.Logger.filterAndLog\_0_Or3Plus(Logger.java:383) at ch.qos.logback.classic.Logger.info(Logger.java:579) ... 復(fù)制代碼
上面論證過(guò)了,WAITING狀態(tài)的線(xiàn)程是不會(huì)消耗CPU的, 所以這里的CPU肯定不是掛起后消耗的, 而是掛起前消耗的.
那是哪段代碼消耗的呢? 答案就在堆棧中的這段代碼:
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199) 復(fù)制代碼
眾所周知, ReentrantLock的底層是使用AQS框架實(shí)現(xiàn)的. AQS大家可能都比較熟悉, 如果不熟悉的話(huà)這里可以大概描述一下AQS:
1. AQS有個(gè)臨界變量state,當(dāng)一個(gè)線(xiàn)程獲取到state==0時(shí), 表示這個(gè)線(xiàn)程進(jìn)入了臨界代碼(獲取到鎖), 并原子地把這個(gè)變量值+1 2. 沒(méi)能進(jìn)入臨界區(qū)(獲取鎖失敗)的線(xiàn)程, 會(huì)利用CAS的方式添加到到CLH隊(duì)列尾去, 并被LockSupport.park掛起. 3. 當(dāng)線(xiàn)程釋放鎖的時(shí)候, 會(huì)喚醒head節(jié)點(diǎn)的下一個(gè)需要喚醒的線(xiàn)程(有些線(xiàn)程cancel了就不需要喚醒了) 4. 被喚醒的線(xiàn)程檢查一下自己的前置節(jié)點(diǎn)是不是head節(jié)點(diǎn)(CLH隊(duì)列的head節(jié)點(diǎn)就是之前拿到鎖的線(xiàn)程節(jié)點(diǎn))的下一個(gè)節(jié)點(diǎn), 如果不是則繼續(xù)掛起, 如果是的話(huà), 與其他線(xiàn)程重新?tīng)?zhēng)奪臨界變量,即重復(fù)第1步 復(fù)制代碼
CAS
在AQS的第2步中, 如果競(jìng)爭(zhēng)鎖失敗的話(huà), 是會(huì)使用CAS樂(lè)觀鎖的方式添加到隊(duì)列尾的, 核心代碼如下:
/** * Inserts node into queue, initializing if necessary. See picture above. * @param node the node to insert * @return node's predecessor */ private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } 復(fù)制代碼
看上面的這段代碼, 設(shè)想在極端情況下(并發(fā)量非常高的情況下), 每一次執(zhí)行compareAndSetTail都失敗(即返回false)的話(huà),那么這段代碼就相當(dāng)是一個(gè)while(true)死循環(huán).
在我們的實(shí)際案例中, 雖然不是極端情況, 但是并發(fā)量也是極高的(每一個(gè)線(xiàn)程每時(shí)每刻都在調(diào)用logback打日志), 所以在某些情況下, 個(gè)別線(xiàn)程會(huì)在這段代碼自旋過(guò)久而長(zhǎng)期占用CPU, 最終導(dǎo)致CPU告警
CAS也是一種樂(lè)觀鎖, 所謂樂(lè)觀就是認(rèn)為競(jìng)爭(zhēng)情況比較少出現(xiàn). 所以CAS是不適合用于鎖競(jìng)爭(zhēng)嚴(yán)重的場(chǎng)景下的,鎖競(jìng)爭(zhēng)嚴(yán)重的場(chǎng)景更適合使用悲觀鎖, 那樣線(xiàn)程被掛起了,會(huì)更加節(jié)省CPU
AQS中線(xiàn)程上下文切換
在實(shí)際的環(huán)境中, 如果臨界區(qū)的代碼執(zhí)行時(shí)間比較短的話(huà)(logback寫(xiě)日志夠短了吧), 上面AQS的第3,第4步也是會(huì)導(dǎo)致CLH隊(duì)列的線(xiàn)程被頻繁喚醒,而又由于搶占鎖失敗頻繁地被掛起. 因此也會(huì)帶來(lái)大量的上下文切換, 消耗系統(tǒng)的cpu資源.
從實(shí)驗(yàn)結(jié)果來(lái)看, 我覺(jué)得這個(gè)原因的可能性更高.
延伸思考
所謂cpu偏高就是指"cpu使用率"過(guò)高. 舉例說(shuō)1個(gè)核的機(jī)器,CPU使用100%, 8個(gè)核使用了800%,都表示cpu被用滿(mǎn)了.那么1核的90%, 8核的700%都可以認(rèn)為cpu使用率過(guò)高了.
cpu被用滿(mǎn)的后果就是操作系統(tǒng)的其他任務(wù)無(wú)法搶占到CPU資源. 在window上的體現(xiàn)就是卡頓,鼠標(biāo)移動(dòng)非常不流暢.在服務(wù)器端的體現(xiàn)就是整個(gè)JVM無(wú)法接受新的請(qǐng)求, 當(dāng)前的處理邏輯也無(wú)法進(jìn)行而導(dǎo)致超時(shí),對(duì)外的表現(xiàn)就是整個(gè)系統(tǒng)不可用.
CPU% = (1 - idleTime / sysTime) * 100 idleTime: CPU空閑時(shí)間 sysTime: CPU在用戶(hù)態(tài)和內(nèi)核態(tài)的使用時(shí)間之和 復(fù)制代碼
cpu是基于時(shí)間片調(diào)度的. 理論上不管一個(gè)線(xiàn)程處理時(shí)間有多長(zhǎng), 它能運(yùn)行的時(shí)間也就是一個(gè)時(shí)間片的時(shí)間, 處理完后就得釋放cpu. 然而它釋放了CPU后, 還是會(huì)立馬又去搶占cpu,而且搶到的概率是一樣的. 所以從應(yīng)用層面看, 有時(shí)還是可以看到這個(gè)線(xiàn)程是占用100%的
最后,從經(jīng)驗(yàn)來(lái)看, 一個(gè)JVM系統(tǒng)的CPU偏高一般就是以下幾個(gè)原因:
代碼中存在死循環(huán)
JVM頻繁GC
加密和解密的邏輯
正則表達(dá)式的處理
頻繁地線(xiàn)程上下文切換
如果真的遇到了線(xiàn)上環(huán)境cpu偏高的問(wèn)題, 不妨先從這幾個(gè)角度進(jìn)行分析.
最最最后, 給大家推薦一個(gè)工具, 可以線(xiàn)上分析jstack的一個(gè)網(wǎng)站, 非常的有用.
網(wǎng)站地址: fastthread.io/
作者:NorthWard
鏈接:https://juejin.cn/post/6844904001067040781
來(lái)源:稀土掘金
著作權(quán)歸作者所有多糠。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處浩考。