1. 程式人生 > >Java執行緒的5個使用技巧

Java執行緒的5個使用技巧

Java執行緒有哪些不太為人所知的技巧與用法?

蘿蔔白菜各有所愛。像我就喜歡Java。學無止境,這也是我喜歡它的一個原因。日常工作中你所用到的工具,通常都有些你從來沒有了解過的東西,比方說某個方法或者是一些有趣的用法。比如說執行緒。沒錯,就是執行緒。或者確切說是Thread這個類。當我們在構建高可擴充套件性系統的時候,通常會面臨各種各樣的併發程式設計的問題,不過我們現在所要講的可能會略有不同。

從本文中你將會看到執行緒提供的一些不太常用的方法及技術。不管你是初學者還是高階使用者或者是Java專家,希望都能看一下哪些是你已經知道的,而哪些是剛瞭解的。如果你認為關於執行緒還有什麼值得分享給大家的,希望能在下面積極回覆。那我們就先開始吧。

初學者

1.執行緒名

程式中的每個執行緒都有一個名字,建立執行緒的時候會給它分配一個簡單的Java字串來作為執行緒名。預設的名字是”Thread-0″, “Thread-1″, “Thread-2″等等。現在有趣的事情來了——Thread提供了兩種方式來設定執行緒名:

執行緒建構函式,下面是最簡單的一個實現:

class SuchThread extends Thread {
Public void run() {
System.out.println ("Hi Mom! " + getName());
}
}
SuchThread wow = new
SuchThread(“much-name”);

執行緒名setter方法:

wow.setName(“Just another thread name”);

沒錯,執行緒名是可變的。因此我們可以在執行時修改它的名字,而不用在初始化的時候就指定好。name欄位其實就是一個簡單的字串物件。也就是說它能達到2³¹-1個字元那麼長(Integer.MAX_VALUE)。這足夠用了。注意這個名字並不是一個唯一性的標識,因此不同的執行緒也可以擁有同樣的執行緒名。還有一點就是,不要把null用作執行緒名,否則會丟擲異常(當然了,”null”還是可以的)。

使用執行緒名來除錯問題

既然可以設定執行緒名,那麼如果遵循一定的命名規則的話,出了問題的時候排查起來就能更容易一些。“Thread-6″這樣的名字看起來就太沒心沒肺了,肯定有比它更好的名字。在處理使用者請求的時候,可以將事務ID追加到執行緒名後面,這樣能顯著減少你排查問題的時間。

“pool-1-thread-1″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800
nid=0x6d03 in Object.wait() [0x000000013ebcc000]“pool-1-thread-1″

這也太嚴肅了吧。我們來看下這是什麼情況,給它起一個好點的名字:

Thread.currentThread().setName(Context + TID + Params + current Time,
…);

現在我們再來執行下jstack,情況便豁然開朗了:

”Queue Processing Thread, MessageID: AB5CAD, type
:AnalyzeGraph, queue:ACTIVE_PROD, Transaction_ID: 5678956,
Start Time: 30/12/2014 17:37″ #17
prio=5 os_prio=31 tid=0x00007f9d620c9800
nid=0x6d03 i Object.wait() [0x000000013ebcc000]

如果我們能知道執行緒在做什麼,這樣當它出問題的時候,至少可以拿到事務ID來開始排查。你可以回溯這個問題,復現它,然後定位問題並搞定它。如果你想知道jstack有什麼給力的用法,可以看下這篇文章。

  1. 執行緒優先順序

執行緒還有一個有意思的屬性就是它的優先順序。執行緒的優先順序介於1 (MINPRIORITY)到10 (MAXPRIORITY)之間,主執行緒預設是5(NORM_PRIORITY)。每個新執行緒都預設繼承父執行緒的優先順序,因此如果你沒有設定過的話,所有執行緒的優先順序都是5。這個是通常被忽視的屬性,我們可以通過getPriority()與setPriority()方法來獲取及修改它的值。執行緒的建構函式裡是沒有這個功能的。

什麼地方會用到優先順序?

當然並不是所有的執行緒都是平等的,有的執行緒需要立即引起CPU的重視,而有些執行緒則只是後臺任務而已。優先順序就是用來把這些告訴給作業系統的執行緒排程器的。在Takipi中,這是我們開發的一錯誤跟蹤及排查的工具,負責處理使用者異常的執行緒的優先順序是MAX_PRIORITY,而那些只是在上報新的部署情況的執行緒,它們的優先順序就要低一些。你可能會覺得優先順序高的執行緒從JVM的執行緒排程器那得到的時間會多一些。但其實並都是這樣的。

在作業系統層面,每一個新執行緒都會對應一個本地執行緒,你所設定的Java執行緒的優先順序會被轉化成本地執行緒的優先順序,這個在各個平臺上是不一樣的。在Linux上,你可以開啟“-XX:+UseThreadPriorities”選項來啟用這項功能。正如前面所說的,執行緒優先順序只是你所提供的一個建議。和Linux本地的優先順序相比,Java執行緒的優先順序並不能覆蓋全所有的級別(Linux共有1到99個優先順序,執行緒的優先順序在是-20到20之間)。最大的好處就是你所設定的優先順序能在每個執行緒獲得的CPU時間上有所體現,不過完全依賴於執行緒優先順序的做法是不推薦的。

免費福利:文章結尾有一份系統的java教程提供大家學習,一起進步

進階篇

3.執行緒本地儲存

這個和前面提到的兩個略有不同。ThreadLocal是在Thread類之外實現的一個功能(java.lang.ThreadLocal),但它會為每個執行緒分別儲存一份唯一的資料。正如它的名字所說的,它為執行緒提供了本地儲存,也就是說你所創建出來變數對每個執行緒例項來說都是唯一的。和執行緒名,執行緒優先順序類似,你可以自定義出一些屬性,就好像它們是儲存在Thread執行緒內部一樣,是不是覺得酷?不過先別高興得太早了,有幾句醜話得先說在前頭。

建立ThreadLocal有兩種推薦方式:要麼是靜態變數,要麼是單例例項中的屬性,這樣可以是非靜態的。注意,它的作用域是全域性的,只不過對訪問它的執行緒而言好像是本地的而已。在下面這個例子中,ThreadLocal裡面儲存了一個數據結構,這樣我們可以很容易地訪問到它:

public static class CriticalData
{
public int transactionId;
public int username;
}
public static final ThreadLocal globalData =
new ThreadLocal();

一旦獲取到了ThreadLocal物件,就可以通過 globalData.set()和globalData.get()方法來對它進行操作了。

全域性變數?這不是什麼好事

也盡然。ThreadLocal可以用來儲存事務ID。如果程式碼中出現未捕獲異常的時候它就相當有用了。最佳實踐是設定一個UncaughtExceptionHandler,這個是Thread類本身就支援的,但是你得自己去實現一下這個介面。一旦執行到了UncaughtExceptionHandler裡,就幾乎沒有任何線索能夠知道到底發生了什麼事情了。這會兒你能獲取到的就只有Thread物件,之前導致異常發生的所有變數都無法再訪問了,因為那些棧幀都已經被彈出了。一旦到了UncaughtExceptionHandler裡,這個執行緒就只剩下最後一口氣了,唯一能抓住的最後一根稻草就是ThreadLocal。

我們來試下這麼做:

System.err.println("Transaction ID " + globalData.get().transactionId);

我們可以將一些與錯誤相關的有價值的上下文資訊給儲存到裡面添。ThreadLocal還有一個更有創意的用法,就是用它來分配一塊特定的記憶體,這樣工作執行緒可以把它當作快取來不停地使用。當然了,這有沒有用得看你在CPU和記憶體之間是怎麼權衡的了。沒錯,ThreadLocal需要注意的就是會造成記憶體空間的浪費。只要執行緒還活著,那麼它就會一直存在,除非你主動釋放否則它是不會被回收的。因此如果使用它的話你最好注意一下,儘量保持簡單。

  1. 使用者執行緒及守護執行緒

我們再回到Thread類。程式中的每個執行緒都會有一個狀態,要麼是使用者狀態,要麼是守護狀態。換句話說,要麼是前臺執行緒要麼是後臺執行緒。主執行緒預設是使用者執行緒,每個新執行緒都會從建立它的執行緒中繼承執行緒狀態。因此如果你把一個執行緒設定成守護執行緒,那麼它所建立的所有執行緒都會被標記成守護執行緒。如果程式中的所有執行緒都是守護執行緒的話,那麼這個程序便會終止。我們可以通過Boolean .setDaemon(true)和.isDaemon()方法來檢視及設定執行緒狀態。

什麼時候會用到守護執行緒?

如果程序不必等到某個執行緒結束才能終止,那麼這個執行緒就可以設定成守護執行緒。這省掉了正常關閉執行緒的那些麻煩事,可以立即將執行緒結束掉。換個角度來說,如果一個正在執行某個操作的執行緒必須要正確地關閉掉否則就會出現不好的後果的話,那麼這個執行緒就應該是使用者執行緒。通常都是些關鍵的事務,比方說,資料庫錄入或者更新,這些操作都是不能中斷的。

專家級

  1. 處理器親和性(Processor Affinity)

這裡要講的會更靠近硬體,也就是說,當軟體遇上了硬體。處理器親和性使得你能夠將執行緒或者程序繫結到特定的CPU核上。這意味著只要是某個特定的執行緒,它就肯定只會在某個特定的CPU核上執行。通常來講如何繫結是由作業系統的執行緒排程器根據它自己的邏輯來決定的,它很可能會將我們前面提到的執行緒優先順序也一併考慮進來。

這麼做的好處在於CPU快取。如果某個執行緒只會在某個核上執行,那麼它的資料恰好在快取裡的概率就大大提高了。如果資料正好就在CPU快取裡,那麼就沒有必要重新再從記憶體里加載了。你所節省的這幾毫秒時間就能用在刀刃上,在這段時間裡程式碼可以馬上開始執行,也就能更好地利用所分配給它的CPU時間。當然了,作業系統層面可能會存在某種優化,硬體架構當然也是個很重要的因素,但利用了處理器的親和性至少能夠減小執行緒切換CPU的機率。

由於這裡摻雜著多種因素,處理器親和性到底對吞吐量有多大的影響,最好還是通過測試的方式來進行證明。也許這個方法並不是總能顯著地提升效能,但至少有一個好處就是吞吐量會相對穩定。親和策略可以細化到非常細的粒度上,這取決於你具體想要什麼。高頻交易行業便是這一策略最能大顯身手的場景之一。

處理器親和性的測試

Java對處理器的親和性並沒有原生的支援,當然了,故事也還沒有就此結束。在Linux上,我們可以通過taskset命令來設定程序的親和性。假設我們現在有一個Java程序在執行,而我們希望將它繫結到某個特定的CPU上:

taskset -c 1 “java AboutToBePinned”

如果是一個已經在運行了的程序:

taskset -c 1

要想深入到執行緒級別還得再加些程式碼才行。所幸的是,有一個開源庫能完成這樣的功能:Java-Thread-Affinity。這個庫是由OpenHFT的Peter Lawrey開發的,實現這一功能最簡單直接的方式應該就是使用這個庫了。我們通過一個例子來快速看下如何繫結某個執行緒,關於該庫的更多細節請參考它在Github上的文件:

AffinityLock al = AffinityLock.acquireLock();

這樣就可以了。關於獲取鎖的一些更高階的選項——比如說根據不同的策略來選擇CPU——在Github上都有詳細的說明。

結論

本文我們介紹了關於執行緒的5點知識:執行緒名,執行緒本地儲存,優先順序,守護執行緒以及處理器親和性。希望這能為你日常工作中所用到的內容開啟一扇新的窗戶,期待你們的反饋!還有什麼有關執行緒處理的方法可以分享給大家的嗎,請不吝賜教。

宣告:本文內容來源於網路,如有侵權請聯絡刪除

老規矩,還是一樣的,只需留言點贊轉發關注後,在我的主頁私信【學習】即可!(不信有人手打)