Java線程有哪些不太爲人所知的技巧與用法?

轉載出處:Java線程的5個使用技巧
英文原文連接html

蘿蔔白菜各有所愛。像我就喜歡Java。學無止境,這也是我喜歡它的一個緣由。平常工做中你所用到的工具,一般都有些你歷來沒有了解過的東西,比方說某個方法或者是一些有趣的用法。好比說線程。沒錯,就是線程。或者確切說是Thread這個類。當咱們在構建高可擴展性系統的時候,一般會面臨各類各樣的併發編程的問題,不過咱們如今所要講的可能會略有不一樣。java

從本文中你將會看到線程提供的一些不太經常使用的方法及技術。無論你是初學者仍是高級用戶或者是Java專家,但願都能看一下哪些是你已經知道的,而哪些是剛瞭解的。若是你認爲關於線程還有什麼值得分享給你們的,但願能在下面積極回覆。那咱們就先開始吧。linux

初學者

1.線程名

程序中的每一個線程都有一個名字,建立線程的時候會給它分配一個簡單的Java字符串來做爲線程名。默認的名字是"Thread-0", "Thread-1", "Thread-2"等等。如今有趣的事情來了——Thread提供了兩種方式來設置線程名:git

  1. 線程構造函數,下面是最簡單的一個實現:github

    class SuchThread extends Thread {
    
        Public void run() {
            System.out.println ("Hi Mom! " + getName());
        }
    
    }
    
    SuchThread wow = new SuchThread("much-name");
  2. 線程名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 in Object.wait() [0x000000013ebcc000]

若是咱們能知道線程在作什麼,這樣當它出問題的時候,至少能夠拿到事務ID來開始排查。你能夠回溯這個問題,復現它,而後定位問題並搞定它。若是你想知道jstack有什麼給力的用法,能夠看下這篇文章

2. 線程優先級

線程還有一個有意思的屬性就是它的優先級。線程的優先級介於1 (MIN_PRIORITY)到10 (MAX_PRIORITY)之間,主線程默認是5(NORM_PRIORITY)。每一個新線程都默認繼承父線程的優先級,所以若是你沒有設置過的話,全部線程的優先級都是5。這個是一般被忽視的屬性,咱們能夠經過getPriority()與setPriority()方法來獲取及修改它的值。線程的構造函數裏是沒有這個功能的。

什麼地方會用到優先級?

固然並非全部的線程都是平等的,有的線程須要當即引發CPU的重視,而有些線程則只是後臺任務而已。優先級就是用來把這些告訴給操做系統的線程調度器的。在Takipi中,這是咱們開發的一錯誤跟蹤及排查的工具,負責處理用戶異常的線程的優先級是MAX_PRIORITY,而那些只是在上報新的部署狀況的線程,它們的優先級就要低一些。你可能會以爲優先級高的線程從JVM的線程調度器那獲得的時間會多一些。但其實並都是這樣的。

在操做系統層面,每個新線程都會對應一個本地線程,你所設置的Java線程的優先級會被轉化成本地線程的優先級,這個在各個平臺上是不同的。在Linux上,你能夠打開「-XX:+UseThreadPriorities」選項來啓用這項功能。正如前面所說的,線程優先級只是你所提供的一個建議。和Linux本地的優先級相比,Java線程的優先級並不能覆蓋全全部的級別(Linux共有1到99個優先級,線程的優先級在是-20到20之間)。最大的好處就是你所設定的優先級能在每一個線程得到的CPU時間上有所體現,不過徹底依賴於線程優先級的作法是不推薦的。

image

進階篇

3.線程本地存儲

這個和前面提到的兩個略有不一樣。ThreadLocal是在Thread類以外實現的一個功能(java.lang.ThreadLocal),但它會爲每一個線程分別存儲一份惟一的數據。正如它的名字所說的,它爲線程提供了本地存儲,也就是說你所建立出來變量對每一個線程實例來講都是惟一的。和線程名,線程優先級相似,你能夠自定義出一些屬性,就好像它們是存儲在Thread線程內部同樣,是否是以爲酷?不過先別高興得太早了,有幾句醜話得先說在前頭。

建立ThreadLocal有兩種推薦方式:要麼是靜態變量,要麼是單例實例中的屬性,這樣能夠是非靜態的。注意,它的做用域是全局的,只不過對訪問它的線程而言好像是本地的而已。在下面這個例子中,ThreadLocal裏面存儲了一個數據結構,這樣咱們能夠很容易地訪問到它:

public static class CriticalData
{
    public int transactionId;
    public int username;
}

public static final ThreadLocal<CriticalData> globalData =
    new ThreadLocal<CriticalData>();

一旦獲取到了ThreadLocal對象,就能夠經過 globalData.set()和globalData.get()方法來對它進行操做了。

全局變量?這不是什麼好事

也盡然。ThreadLocal能夠用來存儲事務ID。若是代碼中出現未捕獲異常的時候它就至關有用了。最佳實踐是設置一個UncaughtExceptionHandler,這個是Thread類自己就支持的,可是你得本身去實現一下這個接口。一旦執行到了UncaughtExceptionHandler裏,就幾乎沒有任何線索可以知道到底發生了什麼事情了。這會兒你能獲取到的就只有Thread對象,以前致使異常發生的全部變量都沒法再訪問了,由於那些棧幀都已經被彈出了。一旦到了UncaughtExceptionHandler裏,這個線程就只剩下最後一口氣了,惟一能抓住的最後一根稻草就是ThreadLocal。

咱們來試下這麼作:

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

咱們能夠將一些與錯誤相關的有價值的上下文信息給存儲到裏面添。ThreadLocal還有一個更有創意的用法,就是用它來分配一塊特定的內存,這樣工做線程能夠把它看成緩存來不停地使用。固然了,這有沒有用得看你在CPU和內存之間是怎麼權衡的了。沒錯,ThreadLocal須要注意的就是會形成內存空間的浪費。只要線程還活着,那麼它就會一直存在,除非你主動釋放不然它是不會被回收的。所以若是使用它的話你最好注意一下,儘可能保持簡單。

4. 用戶線程及守護線程

咱們再回到Thread類。程序中的每一個線程都會有一個狀態,要麼是用戶狀態,要麼是守護狀態。換句話說,要麼是前臺線程要麼是後臺線程。主線程默認是用戶線程,每一個新線程都會從建立它的線程中繼承線程狀態。所以若是你把一個線程設置成守護線程,那麼它所建立的全部線程都會被標記成守護線程。若是程序中的全部線程都是守護線程的話,那麼這個進程便會終止。咱們能夠經過Boolean .setDaemon(true)和.isDaemon()方法來查看及設置線程狀態。

何時會用到守護線程?

若是進程沒必要等到某個線程結束才能終止,那麼這個線程就能夠設置成守護線程。這省掉了正常關閉線程的那些麻煩事,能夠當即將線程結束掉。換個角度來講,若是一個正在執行某個操做的線程必需要正確地關閉掉不然就會出現很差的後果的話,那麼這個線程就應該是用戶線程。一般都是些關鍵的事務,比方說,數據庫錄入或者更新,這些操做都是不能中斷的。

專家級

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

這裏要講的會更靠近硬件,也就是說,當軟件趕上了硬件。處理器親和性使得你可以將線程或者進程綁定到特定的CPU核上。這意味着只要是某個特定的線程,它就確定只會在某個特定的CPU核上執行。一般來說如何綁定是由操做系統的線程調度器根據它本身的邏輯來決定的,它極可能會將咱們前面提到的線程優先級也一併考慮進來。

這麼作的好處在於CPU緩存。若是某個線程只會在某個核上運行,那麼它的數據剛好在緩存裏的機率就大大提升了。若是數據正好就在CPU緩存裏,那麼就沒有必要從新再從內存里加載了。你所節省的這幾毫秒時間就能用在刀刃上,在這段時間裏代碼能夠立刻開始執行,也就能更好地利用所分配給它的CPU時間。固然了,操做系統層面可能會存在某種優化,硬件架構固然也是個很重要的因素,但利用了處理器的親和性至少可以減少線程切換CPU的機率。

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

處理器親和性的測試

Java對處理器的親和性並無原生的支持,固然了,故事也尚未就此結束。在Linux上,咱們能夠經過taskset命令來設置進程的親和性。假設咱們如今有一個Java進程在運行,而咱們但願將它綁定到某個特定的CPU上:

taskset -c 1 「java AboutToBePinned」

若是是一個已經在運行了的進程:

taskset -c 1 <PID>

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

AffinityLock al = AffinityLock.acquireLock();

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

結論

本文咱們介紹了關於線程的5點知識:線程名,線程本地存儲,優先級,守護線程以及處理器親和性。但願這能爲你平常工做中所用到的內容打開一扇新的窗戶,期待大家的反饋!還有什麼有關線程處理的方法能夠分享給你們的嗎,請不吝賜教。

相關文章
相關標籤/搜索