標籤:soc aec hql sleep 共用資源 hashmap code nim 正在執行
改善效能意味著用更少的資源做更多的事情。為了利用並發來提高系統效能,我們需要更有效利用現有的處理器資源,這意味著我們期望使 CPU 儘可能出於忙碌狀態(當然,並不是讓 CPU 週期出於應付無用計算,而是讓 CPU 做有用的事情而忙)。如果程式受限於當前的 CPU 計算能力,那麼我們通過增加更多的處理器或者通過叢集就能提高總的效能。總的來說,效能提高,需要且僅需要解決當前的受限資源,當前受限資源可能是:
- CPU: 如果當前 CPU 已經能夠接近 100% 的利用率,並且代碼商務邏輯無法再簡化,那麼說明該系統的效能以及達到上線,只有通過增加處理器來提高效能
- 其他資源:比如串連數等。可以修改代碼,盡量利用 CPU,可以獲得極大的效能提升
如果你的系統有如下的特點,說明系統存在效能瓶頸:
一個好的程式,應該是能夠充分利用 CPU 的。如果一個程式在單 CPU 的機器上無論多大壓力都不能使 CPU 使用率接近 100%,說明這個程式設計有問題。一個系統的效能瓶頸分析過程大致如下:
- 先進性單流程的效能瓶頸分析,受限讓單流程的效能達到最優。
- 進行整體效能瓶頸分析。因為單流程效能最優,不一定整個系統效能最優。在多線程場合下,鎖爭用?給也會導致效能下降。
高效能在不同的應用場合下,有不同的含義:
- 有的場合高效能意味著使用者速度的體驗,如介面操作等
- 有的場合,高輸送量意味著高效能,如簡訊或者多媒體訊息,系統更看重輸送量,而對每一個訊息的處理時間不敏感
- 有的場合,是二者的結合
效能調優的終極目標是:系統的 CPU 利用率接近 100%,如果 CPU 沒有被充分利用,那麼有如下幾個可能:
- 施加的壓力不足
- 系統存在瓶頸
1 常見的效能瓶頸1.1 由於不恰當的同步導致的資源爭用1.1.1 不相關的兩個函數,公用了一個鎖,或者不同的共用變數共用了同一個鎖,無謂地製造出了資源爭用
下面是一種常見的錯誤
兩個不相干的方法(沒有使用同一個共用變數),共用了 this 鎖,導致人為的資源競爭上面的代碼將 synchronized 加在類的每一個方法上面,違背了保護什麼鎖什麼的原則。對於無共用資源的方法,使用了同一個鎖,人為造成了不必要的等待。Java 預設提供了 this 鎖,這樣很多人喜歡直接在方法上使用 synchronized 加鎖,很多情況下這樣做是不恰當的,如果不考慮清楚就這樣做,很容易造成鎖粒度過大:
- 即使一個方法中的代碼也不是處處需要鎖保護的。如果整個方法使用了 synchronized,那麼很可能就把 synchronized 的範圍給人為擴大了。在方法層級上加鎖,是一種粗獷的鎖使用習慣。
上面的代碼應該變成下面
這樣會導致當前線程佔用鎖的時間過長,其他需要鎖的線程只能等待,最終導致效能受到極大影響1.1.2 鎖的粒度過大,對共用資源訪問完成後,沒有將後續的代碼放在synchronized 同步代碼塊之外
單 CPU 場合 將耗時操作拿到同步塊之外,有的情況下可以提升效能,有的場合則不能:上面的代碼,會導致一個線程長時間佔有鎖,而在這麼長的時間裡其他線程只能等待,這種寫法在不同的場合下有不同的提升餘地:
-
- 同步塊的耗時代碼是 CPU 密集型代碼(純 CPU 運算等),不存在磁碟 IO/網路 IO 等低 CPU 限定的代碼,這種情況下,由於 CPU 執行這段代碼是 100% 的使用率,因此縮小同步塊也不會帶來任何效能上的提升。但是,同時縮小同步塊也不會帶來效能上的下降
- 同步塊中的耗時代碼屬於磁碟/網路 IO等低 CPU 限定的代碼,噹噹前線程正在執行不消耗 CPU 的代碼時,這時候 CPU 是閒置,如果此時讓 CPU 忙碌起來,可以帶來整體效能上的提升,所以在這種情境下,將耗時操作的代碼放在同步之外,肯定是可以提高整個效能的(?)
- 多 CPU 場合 將耗時的操作拿到同步塊之外,總是可以提升效能
- 同步塊的耗時代碼是 CPU 密集型代碼(純 CPU 運算等),不存在磁碟 IO/網路 IO 等低 CPU 限定的代碼,這種情況下,由於是多 CPU,其他 CPU也許是閒置,因此縮小同步塊可以讓其他線程馬上得到執行這段代碼,可以帶來效能的提升
- 同步塊中的耗時代碼屬於磁碟/網路 IO等低 CPU 限定的代碼,噹噹前線程正在執行不消耗 CPU 的代碼時,這時候總有 CPU 是閒置,如果此時讓 CPU 忙碌起來,可以帶來整體效能上的提升,所以在這種情境下,將耗時操作的代碼放在同步塊之外,肯定是可以提高整個效能的
不管如何,縮小同步範圍,對系統沒有任何不好的影響,大多數情況下,會帶來效能的提升,所以一定要縮小同步範圍,因此上面的代碼應該改為
Sleep 的濫用,尤其是輪詢中使用 sleep,會讓使用者明顯感覺到延遲,可以修改為 notify 和 wait1.1.3 其他問題
- String + 的濫用,每次 + 都會產生一個臨時對象,並有資料的拷貝
- 不恰當的執行緒模式
- 效率地下的 SQL 陳述式或者不恰當的資料庫設計
- 不恰當的 GC 參數設定導致的效能低下
- 線程數量不足
- 記憶體流失導致的頻繁 GC
2.2 效能瓶頸分析的手段和工具
上面提到的這些原因形成的效能瓶頸,都可以通過線程堆棧分析,找到根本原因。
2.2.1 如何去類比,發現效能瓶頸
效能瓶頸的幾個特徵:
- 當前的效能瓶頸只有一處,只有當解決了這一處,才知道下一處。沒有解決當前效能瓶頸,下一處效能瓶頸是不會出現的。如所示,第二段是瓶頸,解決第二段的瓶頸後,第一段就變成了瓶頸,如此反覆找到所有的效能瓶頸
- 效能瓶頸是動態,低負載下不是瓶頸的地方,高負載下可能成為瓶頸。由於 JProfile 等效能剖析工具依附在 JVM 上帶來的開銷,使系統根本就無法達到該瓶頸出現時需要的效能,因此在這種情境下線程堆棧分析才是一個真正有效方法
鑒於效能瓶頸的以上特點,進行效能類比的時候,一定要使用比系統當前稍高的壓力下進行類比,否則效能瓶頸不會出現。具體步驟如下:
2.2.2 如何通過線程堆棧識別效能瓶頸
通過線程堆棧,可以很容易的識別多線程場合下高負載的時候才會出現的效能瓶頸。一旦一個系統出現效能瓶頸,最重要的就是識別效能瓶頸,然後根據識別的效能瓶頸進行修改。一般多線程系統,先按照線程的功能進行歸類(組),把執行相同功能代碼的線程作為一組進行分析。當使用堆棧進行分析的時候,以這一組線程進行統計學分析。如果一個線程池為不同的功能代碼服務,那麼將整個線程池的線程作為一組進行分析即可。
一般一個系統一旦出現效能瓶頸,從堆棧上分析,有如下三種最為典型的堆棧特徵:
- 絕大多數線程的堆棧都表現為在同一個調用上下文,且只剩下非常少的空閑線程。可能的原因如下:
- 線程的數量過少
- 鎖的粒度過大導致的鎖競爭
- 資源競爭
- 鎖範圍中有大量耗時操作
- 遠程通訊的對方處理緩慢
- 絕大多數線程出於等待狀態,只有幾個工作的線程,總體效能上不去。可能的原因是,系統存在關鍵路徑,關鍵路徑已經達到瓶頸
- 線程總的數量很少(有些線程池的實現是按需建立線程,可能程式中建立線程
一個例子
| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283 |
"Thread-243" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable[0xaeedb000..0xaeedc480]at java.net.SocketInputStream.socketRead0(Native Method)at java.net.SocketInputStream.read(SocketInputStream.java:129)at oracle.net.ns.Packet.receive(Unknown Source)... ...at oracle.jdbc.driver.LongRawAccessor.getBytes()at oracle.jdbc.driver.OracleResultSetImpl.getBytes()- locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl)at oracle.jdbc.driver.OracleResultSet.getBytes(O)... ...at org.hibernate.loader.hql.QueryLoader.list()at org.hibernate.hql.ast.QueryTranslatorImpl.list()... ...at com.wes.NodeTimerOut.execute(NodeTimerOut.java:175)at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707)at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)- locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl)at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)at com.wes.threadpool.PooledExecutorEx$Worker.run()at java.lang.Thread.run(Thread.java:595)"Thread-248" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable[0xaeedb000..0xaeedc480]at java.net.SocketInputStream.socketRead0(Native Method)at java.net.SocketInputStream.read(SocketInputStream.java:129)at oracle.net.ns.Packet.receive(Unknown Source)... ...at oracle.jdbc.driver.LongRawAccessor.getBytes()at oracle.jdbc.driver.OracleResultSetImpl.getBytes()- locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl)at oracle.jdbc.driver.OracleResultSet.getBytes(O)... ...at org.hibernate.loader.hql.QueryLoader.list()at org.hibernate.hql.ast.QueryTranslatorImpl.list()... ...at com.wes.NodeTimerOut.execute(NodeTimerOut.java:175)at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707)at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)- locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl)at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)at com.wes.threadpool.PooledExecutorEx$Worker.run()at java.lang.Thread.run(Thread.java:595)... ..."Thread-238" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait()[0xaec56000..0xaec57700]at java.lang.Object.wait(Native Method)at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104)- locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList)at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642)... ...at org.hibernate.impl.SessionImpl.list()at org.hibernate.impl.SessionImpl.find()at com.wes.DBSessionMediatorImpl.find()at com.wes.ResourceDBInteractorImpl.getCallBackObj()at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152)at com.wes.timer.TimerTaskImpl.executeAll()at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)- locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl)at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)at com.wes.threadpool.PooledExecutorEx$Worker.run()at java.lang.Thread.run(Thread.java:595) "Thread-233" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait()[0xaec56000..0xaec57700] at java.lang.Object.wait(Native Method)at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104)- locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList)at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642)... ...at org.hibernate.impl.SessionImpl.list()at org.hibernate.impl.SessionImpl.find()at com.wes.DBSessionMediatorImpl.find()at com.wes.ResourceDBInteractorImpl.getCallBackObj()at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152)at com.wes.timer.TimerTaskImpl.executeAll()at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)- locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl)at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)at com.wes.threadpool.PooledExecutorEx$Worker.run()at java.lang.Thread.run(Thread.java:595)... ... |
從堆棧看,有 51 個(socket)訪問,其中有 50 個是 JDBC 資料庫訪問。其他方法被阻塞在 java.lang.Object.wait() 方法上。
2.2.3 其他提高效能的方法
減少鎖的粒度,比如 ConcurrentHashMap 的實現預設使用 16 個鎖的 Array(有一個副作用:鎖整個容器會很費力,可以添加一個全域鎖)
2.2.4 效能調優的終結條件
效能調優總有一個終止條件,如果系統滿足如下兩個條件,即可終止:
- 演算法足夠最佳化
- 沒有線程/資源的使用不當而導致的 CPU 利用不足
通過 Java 線程堆棧進行效能瓶頸分析