1. 程式人生 > >Java高效能程式設計實戰 --- 執行緒封閉與ThreadLocal

Java高效能程式設計實戰 --- 執行緒封閉與ThreadLocal

1 執行緒封閉

多執行緒訪問共享可變資料時,涉及到執行緒間資料同步的問題。並不是所有時候,都要用到 共享資料,所以執行緒封閉概念就提出來了。

資料都被封閉在各自的執行緒之中,就不需要同步,這種通過將資料封閉線上程中而避免使 用同步的技術稱為執行緒封閉

避免併發異常最簡單的方法就是執行緒封閉 即 把物件封裝到一個執行緒裡,只有該執行緒能看到此物件; 那麼該物件就算非執行緒安全,也不會出現任何併發安全問題.

1.1 棧封閉

區域性變數的固有屬性之一就是封閉線上程中。 它們位於執行執行緒的棧中,其他執行緒無法訪問這個棧

1.2 使用ThreadLocal是實現執行緒封閉的最佳實踐.

ThreadLocal是Java裡一種特殊的變數。

它是一個執行緒級變數,每個執行緒都有一個ThreadLocal, 就是每個執行緒都擁有了自己獨立的一個變數, 競爭條件被徹底消除了,在併發模式下是絕對安全的變數。

  • 用法
ThreadLocal<t> var = new ThreadLocal<t>();

會自動在每一個執行緒上建立一個T的副本,副本之間彼此獨立,互不影響。 可以用 ThreadLocal 儲存一些引數, 以便線上程中多個方法中使用,用來代替方法傳參的做法。

例項 ThreadLocal內部維護了一個Map,Map的key是每個執行緒的名稱,Map的值就是我們要封閉的物件. 每個執行緒中的物件都對應著Map中一個值,也就是ThreadLocal

利用Map實現了物件的執行緒封閉.

對於CS遊戲,開始時,每個人能夠領到一把槍,槍把上有三個數字:子彈數、殺敵數、自己的命數,為其設定的初始值分別為1500、0、10.

設戰場上的每個人都是一個執行緒,那麼這三個初始值寫在哪裡呢? 如果每個執行緒都寫死這三個值,萬一將初始子彈數統一改成 1000發呢? 如果共享,那麼執行緒之間的併發修改會導致資料不準確. 能不能構造這樣一個物件,將這個物件設定為共享變數,統一設定初始值,但是每個執行緒對這個值的修改都是互相獨立的.這個物件就是ThreadLocal >注意不能將其翻譯為執行緒本地化或本地執行緒 英語恰當的名稱應該叫作:CopyValueIntoEveryThread

示例程式碼

> 實在難以理解的,可以理解為,JVM維護了一個Map<thread, t>,每個執行緒要用這個T的時候,用當前的執行緒去Map裡面取。僅作為一個概念理解

該示例中,無 set 操作,那麼初始值又是如何進入每個執行緒成為獨立拷貝的呢? 首先,雖然ThreadLocal在定義時重寫了initialValue() ,但並非是在BULLET_ NUMBER_ THREADLOCAL物件載入靜態變數的時候執行; 而是每個執行緒在ThreadLocal.get()時都會執行到; 其原始碼如下 ThreadLocal # get()

每個執行緒都有自己的ThreadLocalMap; 如果map ==null,則直接執行setInitialValue(); 如果 map 已建立,就表示 Thread 類的threadLocals 屬性已初始化完畢; 如果 e==null,依然會執行到setinitialValue() setinitialValue() 的原始碼如下: 這是一個保護方法,CsGameByThreadLocal中初始化ThreadLocal物件時已覆寫value = initialValue() ; getMap的原始碼就是提取執行緒物件t的ThreadLocalMap屬性: t. threadLocals.

> 在CsGameByThreadLocal第1處,使用了ThreadLocalRandom 生成單獨的Random例項; 該類在JDK7中引入,它使得每個執行緒都可以有自己的隨機數生成器; 我們要避免Random例項被多執行緒使用,雖然共享該例項是執行緒安全的,但會因競爭同一seed而導致效能下降.

我們已經知道了ThreadLocal是每一個執行緒單獨持有的; 因為每一個執行緒都有獨立的變數副本,其他執行緒不能訪問,所以不存線上程安全問題,也不會影響程式的執行效能. ThreadLocal物件通常是由private static修飾的,因為都需要複製到本地執行緒,所以非static作用不大; 不過,ThreadLocal無法解決共享物件的更新問題,下面的例項將證明這點. 因為CsGameByThreadLocal中使用的是Integer 不可變物件,所以可使用相同的編碼方式來操作一下可變物件看看 輸出的結果是亂序不可控的,所以使用某個引用來操作共享物件時,依然需要進行執行緒同步 ThreadLocal和Thread的類圖

ThreadLocal 有個靜態內部類ThreadLocalMap,它還有一個靜態內部類Entry; 在Thread中的ThreadLocalMap屬性的賦值是在ThreadLocal類中的createMap.

ThreadLocal ThreadLocalMap有三組對應的方法: get()、set()和remove(); 在ThreadLocal中對它們只做校驗和判斷,最終的實現會落在ThreadLocalMap.. Entry繼承自WeakReference,只有一個value成員變數,它的key是ThreadLocal物件

再從棧與堆的記憶體角度看看兩者的關係 ThreadLocal的弱引用路線圖 一個Thread有且僅有一個ThreadLocalMap物件 一個Entry物件的 key 弱引用指向一個ThreadLocal物件 一個ThreadLocalMap 物件儲存多個Entry 物件 一個ThreadLocal 物件可被多個執行緒共享 ThreadLocal物件不持有Value,Value 由執行緒的Entry 物件持有.

Entry 物件原始碼如下

所有的Entry物件都被ThreadLocalMap類例項化物件threadLocals持有; 當執行緒執行完畢時,執行緒內的例項屬性均會被垃圾回收,弱引用的ThreadLocal,即使執行緒正在執行,只要ThreadLocal物件引用被置成null,Entry的Key就會自動在下一次Y - GC時被垃圾回收; 而在ThreadLocal使用set()/get()時,又會自動將那些key=null的value 置為null,使value能夠被GC,避免記憶體洩漏,現實很骨感, ThreadLocal如原始碼註釋所述: ThreadLocal物件通常作為私有靜態變數使用,那麼其生命週期至少不會隨著執行緒結束而結束.

三個重要方法:

  • set() 如果沒有set操作的ThreadLocal, 很容易引起髒資料問題
  • get() 始終沒有get操作的ThreadLocal物件是沒有意義的
  • remove() 如果沒有remove操作,則容易引起記憶體洩漏

如果ThreadLocal是非靜態的,屬於某個執行緒例項,那就失去了執行緒間共享的本質屬性; 那麼ThreadLocal到底有什麼作用呢? 我們知道,區域性變數在方法內各個程式碼塊間進行傳遞,而類變數在類內方法間進行傳遞; 複雜的執行緒方法可能需要呼叫很多方法來實現某個功能,這時候用什麼來傳遞執行緒內變數呢? 即ThreadLocal,它通常用於同一個執行緒內,跨類、跨方法傳遞資料; 如果沒有ThreadLocal,那麼相互之間的資訊傳遞,勢必要靠返回值和引數,這樣無形之中,有些類甚至有些框架會互相耦合; 通過將Thread構造方法的最後一個引數設定為true,可以把當前執行緒的變數繼續往下傳遞給它建立的子執行緒

public Thread (ThreadGroup group, Runnable target, String name,long stackSize, boolean inheritThreadLocals) [
   this (group, target, name,  stackSize, null, inheritThreadLocals) ;
}

parent為其父執行緒

if (inheritThreadLocals &amp;&amp; parent. inheritableThreadLocals != null)
      this. inheritableThreadLocals = ThreadLocal. createInheritedMap (parent. inheritableThreadLocals) ;

createlnheritedMap()其實就是呼叫ThreadLocalMap的私有構造方法來產生一個例項物件,把父執行緒中不為null的執行緒變數都拷貝過來

private ThreadLocalMap (ThreadLocalMap parentMap) {
    // table就是儲存
    Entry[] parentTable = parentMap. table;
    int len = parentTable. length;
    setThreshold(len) ;
    table = new Entry[len];

    for (Entry e : parentTable) {
      if (e != null) {
        ThreadLocal<object> key = (ThreadLocal<object>) e.get() ;
        if (key != null) {
          object value = key. childValue(e.value) ;
          Entry c = new Entry(key, value) ;
          int h = key. threadLocalHashCode &amp; (len - 1) ;
          while (table[h] != null)
            h = nextIndex(h, len) ;
          table[h] = C;
          size++;
        }
    }
}

很多場景下可通過ThreadLocal來透傳全域性上下文的; 比如用ThreadLocal來儲存監控系統的某個標記位,暫且命名為traceld. 某次請求下所有的traceld都是一致的,以獲得可以統一解析的日誌檔案; 但在實際開發過程中,發現子執行緒裡的traceld為null,跟主執行緒的traceld並不一致,所以這就需要剛才說到的InheritableThreadLocal來解決父子執行緒之間共享執行緒變數的問題,使整個連線過程中的traceld一致. 示例程式碼如下

import org.apache.commons.lang3.StringUtils;

/**
 * @author sss
 * @date 2019/1/17
 */
public class RequestProcessTrace {

    private static final InheritableThreadLocal<fulllinkcontext> FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL
            = new InheritableThreadLocal<fulllinkcontext>();

    public static FullLinkContext getContext() {
        FullLinkContext fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
        if (fullLinkContext == null) {
            FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.set(new FullLinkContext());
            fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
        }
        return fullLinkContext;
    }

    private static class FullLinkContext {
        private String traceId;

        public String getTraceId() {
            if (StringUtils.isEmpty(traceId)) {
                FrameWork.startTrace(null, "JavaEdge");
                traceId = FrameWork.getTraceId();
            }
            return traceId;
        }

        public void setTraceId(String traceId) {
            this.traceId = traceId;
        }
    }

}

使用ThreadLocalInheritableThreadLocal透傳上下文時,需要注意執行緒間切換、異常傳輸時的處理,避免在傳輸過程中因處理不當而導致的上下文丟失.

最後,SimpleDateFormat 是非執行緒安全的類,定義為static,會有資料同步風險. 通過原始碼可以看出,SimpleDateFormat 內部有一個Calendar物件; 在日期轉字串或字串轉日期的過程中,多執行緒共享時很可能產生錯誤; 推薦使用 ThreadLocal,讓每個執行緒單獨擁有這個物件.

ThreadLocal的副作用

為了使執行緒安全地共享某個變數,JDK給出了ThreadLocal. 但ThreadLocal的主要問題是會產生髒資料和記憶體洩漏; 這兩個問題通常是線上程池的執行緒中使用ThreadLocal引發的,因為執行緒池有執行緒複用和記憶體常駐兩是線上程池的執行緒中使用ThreadLocal 引發的,因為執行緒池有執行緒複用和記憶體常駐兩個特點

1 髒資料

執行緒複用會產生髒資料; 由於執行緒池會重用 Thread 物件,與 Thread 繫結的靜態屬性 ThreadLoca l變數也會被重用. 如果在實現的執行緒run()方法中不顯式呼叫remove()清理與執行緒相關的ThreadLocal資訊,那麼若下一個執行緒不呼叫set(),就可能get() 到重用的執行緒資訊; 包括ThreadLocal所關聯的執行緒物件的value值.

髒讀問題其實十分常見. 比如,使用者A下單後沒有看到訂單記錄,而使用者B卻看到了使用者A的訂單記錄. 通過排查發現是由於 session 優化引發. 在原來的請求過程中,使用者每次請求Server,都需要通過 sessionId 去快取裡查詢使用者的session資訊,這樣無疑增加了一次呼叫. 因此,工程師決定採用某框架來快取每個使用者對應的SecurityContext, 它封裝了session 相關資訊. 優化後雖然會為每個使用者新建一個 session 相關的上下文,但由於Threadlocal沒有線上程處理結束時及時remove(); 在高併發場景下,執行緒池中的執行緒可能會讀取到上一個執行緒快取的使用者資訊.

  • 示例程式碼 輸出結果

2 記憶體洩漏

在原始碼註釋中提示使用static關鍵字來修飾ThreadLocal. 在此場景下,寄希望於ThreadLocal物件失去引用後,觸發弱引用機制來回收EntryValue就不現實了. 在上例中,如果不進行remove(),那麼當該執行緒執行完成後,通過ThreadLocal物件持有的String物件是不會被釋放的.

  • 以上兩個問題的解決辦法很簡單 每次用完ThreadLocal時,及時呼叫remove()清理

What is ThreadLocal

該類提供了執行緒區域性 (thread-local) 變數; 這些變數不同於它們的普通對應物,因為訪問某變數(通過其 get /set 方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本.

ThreadLocal 例項通常是類中的 private static 欄位,希望將狀態與某一個執行緒(e.g. 使用者 ID 或事務 ID)相關聯.

一個以ThreadLocal物件為鍵、任意物件為值的儲存結構; 有點像HashMap,可以儲存"key : value"鍵值對,但一個ThreadLocal只能儲存一個鍵值對,各個執行緒的資料互不干擾. 該結構被附帶線上程上,也就是說一個執行緒可以根據一個ThreadLocal物件查詢到繫結在這個執行緒上的一個值.

ThreadLocal<string> localName = new ThreadLocal();
localName.set("JavaEdge");
String name = localName.get();

線上程A中初始化了一個ThreadLocal物件localName,並set了一個值JavaEdge; 同時線上程A中通過get可拿到之前設定的值; 但是如果線上程B中,拿到的將是一個null.

因為ThreadLocal保證了各個執行緒的資料互不干擾 看看set(T value)和get()方法的原始碼 返回當前執行緒該執行緒區域性變數副本中的值 設定此執行緒區域性變數的當前執行緒的副本到指定的值,大多數的子類都不需要重寫此方法

Thread#threadLocals

可見,每個執行緒中都有一個ThreadLocalMap

  • 執行set時,其值是儲存在當前執行緒的threadLocals變數
  • 執行get時,從當前執行緒的threadLocals變數獲取

所以線上程A中set的值,是執行緒B永遠得不到的 即使線上程B中重新set,也不會影響A中的值; 保證了執行緒之間不會相互干擾.

追尋本質 - 結構

從名字上看猜它類似HashMap,但在ThreadLocal中,並無實現Map介面

  • ThreadLoalMap中,也是初始化一個大小為16的Entry陣列

  • Entry節點物件用來儲存每一個key-value鍵值對 這裡的key 恆為 ThreadLocal; 通過ThreadLocalset(),把ThreadLocal物件自身當做key,放進ThreadLoalMap ThreadLoalMapEntry繼承WeakReference 和HashMap很不同,Entry中沒有next欄位,所以不存在連結串列情形.

hash衝突

無連結串列,那發生hash衝突時何解?

先看看ThreadLoalMap插入一個 key/value 的實現

  • 每個ThreadLocal物件都有一個hash值 - threadLocalHashCode
  • 每初始化一個ThreadLocal物件,hash值就增加一個固定大小

在插入過程中,根據ThreadLocal物件的hash值,定位至table中的位置i. 過程如下

  • 若當前位置為空,就初始化一個Entry物件置於i;
  • 位置i已有物件
    • 若該Entry物件的key正是將設定的key,覆蓋其value(和HashMap 處理相同);
    • 若和即將設定的key 無關,則尋找下一個空位

如此,在get時,也會根據ThreadLocal物件的hash值,定位到table中的位置.然後判斷該位置Entry物件中的key是否和get的key一致,如果不一致,就判斷下一個位置.

可見,set和get如果衝突嚴重的話,效率很低,因為ThreadLoalMap是Thread的一個屬性,所以即使在自己的程式碼中控制了設定的元素個數,但還是不能控制其它程式碼的行為

記憶體洩露

ThreadLocal可能導致記憶體洩漏,為什麼? 先看看Entry的實現:

static class Entry extends WeakReference<threadlocal<?>&gt; {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<!--?--> k, Object v) {
        super(k);
        value = v;
    }
}

通過之前的分析已經知道,當使用ThreadLocal儲存一個value時,會在ThreadLocalMap中的陣列插入一個Entry物件,按理說key-value都應該以強引用儲存在Entry物件中,但在ThreadLocalMap的實現中,key被儲存到了WeakReference物件中

這就導致了一個問題,ThreadLocal在沒有外部強引用時,發生GC時會被回收,如果建立ThreadLocal的執行緒一直持續執行,那麼這個Entry物件中的value就有可能一直得不到回收,發生記憶體洩露。

避免記憶體洩露

既然發現有記憶體洩露的隱患,自然有應對策略,在呼叫ThreadLocal的get()、set()可能會清除ThreadLocalMap中key為null的Entry物件,這樣對應的value就沒有GC Roots可達了,下次GC的時候就可以被回收,當然如果呼叫remove方法,肯定會刪除對應的Entry物件。

如果使用ThreadLocal的set方法之後,沒有顯示的呼叫remove方法,就有可能發生記憶體洩露,所以養成良好的程式設計習慣十分重要,使用完ThreadLocal之後,記得呼叫remove方法。

    ThreadLocal<string> localName = new ThreadLocal();
    try {
        localName.set("JavaEdge");
        // 其它業務邏輯
    } finally {
        localName.remove();
    }

題外小話

首先,ThreadLocal 不是用來解決共享物件的多執行緒訪問問題的. 一般情況下,通過set() 到執行緒中的物件是該執行緒自己使用的物件,其他執行緒是不需要訪問的,也訪問不到的; 各個執行緒中訪問的是不同的物件.

另外,說ThreadLocal使得各執行緒能夠保持各自獨立的一個物件; 並不是通過set()實現的,而是通過每個執行緒中的new 物件的操作來建立的物件,每個執行緒建立一個,不是什麼物件的拷貝或副本。 通過set()將這個新建立的物件的引用儲存到各執行緒的自己的一個map中,每個執行緒都有這樣一個map; 執行get()時,各執行緒從自己的map中取出放進去的物件,因此取出來的是各自執行緒中的物件. ThreadLocal例項是作為map的key來使用的.

如果set()進去的東西本來就是多個執行緒共享的同一個物件; 那麼多個執行緒的get()取得的還是這個共享物件本身,還是有併發訪問問題。

Hibernate中典型的 ThreadLocal 應用

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

首先判斷當前執行緒中有沒有放入 session,如果還沒有,那麼通過sessionFactory().openSession()來建立一個session; 再將session set()到執行緒中,實際是放到當前執行緒的ThreadLocalMap; 這時,對於該 session 的唯一引用就是當前執行緒中的那個ThreadLocalMap; threadSession 作為這個值的key,要取得這個 session 可以通過threadSession.get(); 裡面執行的操作實際是先取得當前執行緒中的ThreadLocalMap; 然後將threadSession作為key將對應的值取出. 這個 session 相當於執行緒的私有變數,而不是public的.

顯然,其他執行緒中是取不到這個session的,他們也只能取到自己的ThreadLocalMap中的東西。要是session是多個執行緒共享使用的,那還不亂套了.

如果不用ThreadLocal怎麼實現呢?

可能就要在action中建立session,然後把session一個個傳到service和dao中,這可夠麻煩的; 或者可以自己定義一個靜態的map,將當前thread作為key,建立的session作為值,put到map中,應該也行,這也是一般人的想法. 但事實上,ThreadLocal的實現剛好相反,它是在每個執行緒中有一個map,而將ThreadLocal例項作為key,這樣每個map中的項數很少,而且當執行緒銷燬時相應的東西也一起銷燬了

總之,ThreadLocal不是用來解決物件共享訪問問題的; 而主要是提供了保持物件的方法和避免參數傳遞的方便的物件訪問方式

  • 每個執行緒中都有一個自己的ThreadLocalMap類物件; 可以將執行緒自己的物件保持到其中,各管各的,執行緒可以正確的訪問到自己的物件.
  • 將一個共用的ThreadLocal靜態例項作為key,將不同物件的引用儲存到不同執行緒的ThreadLocalMap中,然後線上程執行的各處通過這個靜態ThreadLocal例項的get()方法取得自己執行緒儲存的那個物件,避免了將這個物件作為引數傳遞的麻煩.

當然如果要把本來執行緒共享的物件通過set()放到執行緒中也可以,可以實現避免參數傳遞的訪問方式; 但是要注意get()到的是那同一個共享物件,併發訪問問題要靠其他手段來解決; 但一般來說執行緒共享的物件通過設定為某類的靜態變數就可以實現方便的訪問了,似乎沒必要放到執行緒中

ThreadLocal的應用場合

我覺得最適合的是按執行緒多例項(每個執行緒對應一個例項)的物件的訪問,並且這個物件很多地方都要用到。

可以看到ThreadLocal類中的變數只有這3個int型:

private final int threadLocalHashCode = nextHashCode();  
private static AtomicInteger nextHashCode =
        new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647; 

而作為ThreadLocal例項的變數只有 threadLocalHashCode nextHashCodeHASH_INCREMENT 是ThreadLocal類的靜態變數 實際上

  • HASH_INCREMENT是一個常量,表示了連續分配的兩個ThreadLocal例項的threadLocalHashCode值的增量
  • nextHashCode 表示了即將分配的下一個ThreadLocal例項的threadLocalHashCode 的值

看一下建立一個ThreadLocal例項即new ThreadLocal()時做了哪些操作,構造方法ThreadLocal()裡什麼操作都沒有,唯一的操作是這句

private final int threadLocalHashCode = nextHashCode();  

那麼nextHashCode()做了什麼呢

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

就是將ThreadLocal類的下一個hashCode值即nextHashCode的值賦給例項的threadLocalHashCode,然後nextHashCode的值增加HASH_INCREMENT這個值。.

因此ThreadLocal例項的變數只有這個threadLocalHashCode,而且是final的,用來區分不同的ThreadLocal例項; ThreadLocal類主要是作為工具類來使用,那麼set()進去的物件是放在哪兒的呢?

看一下上面的set()方法,兩句合併一下成為

ThreadLocalMap map = Thread.currentThread().threadLocals;  

這個ThreadLocalMap 類是ThreadLocal中定義的內部類,但是它的例項卻用在Thread類中:

public class Thread implements Runnable {  
    ......  
  
    /* ThreadLocal values pertaining to this thread. This map is maintained 
     * by the ThreadLocal class. */  
    ThreadLocal.ThreadLocalMap threadLocals = null;    
    ......  
} 

再看這句:

if (map != null)  
    map.set(this, value);  

也就是將該ThreadLocal例項作為key,要保持的物件作為值,設定到當前執行緒的ThreadLocalMap 中,get()方法同樣看了程式碼也就明白了.

參考

《碼出高效:Java開發手冊》 </string></threadlocal<?></string></fulllinkcontext></fulllinkcontext></object></object></thread,>