Java高性能編程實戰 --- 線程封閉與ThreadLocal

1 線程封閉

多線程訪問共享可變數據時,涉及到線程間數據同步的問題。並非全部時候,都要用到共享數據,因此線程封閉概念就提出來了。apache

數據都被封閉在各自的線程之中,就不須要同步,這種經過將數據封閉在線程中而避免使用同步的技術稱爲線程封閉編程

避免併發異常最簡單的方法就是線程封閉即 把對象封裝到一個線程裏,只有該線程能看到此對象;那麼該對象就算非線程安全,也不會出現任何併發安全問題.數組

1.1 棧封閉

局部變量的固有屬性之一就是封閉在線程中。它們位於執行線程的棧中,其餘線程沒法訪問這個棧緩存

1.2 使用ThreadLocal是實現線程封閉的最佳實踐.

ThreadLocal是Java裏一種特殊的變量。它是一個線程級變量,每一個線程都有一個ThreadLocal, 就是每一個線程都擁有了本身獨立的一個變量,競爭條件被完全消除了,在併發模式下是絕對安全的變量。安全

  • 用法
    -
ThreadLocal<T> var = new ThreadLocal<T>();複製代碼

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

實例ThreadLocal內部維護了一個Map,Map的key是每一個線程的名稱,Map的值就是咱們要封閉的對象.每一個線程中的對象都對應着Map中一個值,也就是ThreadLocal利用Map實現了對象的線程封閉.多線程

對於CS遊戲,開始時,每一個人可以領到一把槍,槍把上有三個數字:子彈數、殺敵數、本身的命數,爲其設置的初始值分別爲1500、0、10.併發

設戰場上的每一個人都是一個線程,那麼這三個初始值寫在哪裏呢?若是每一個線程都寫死這三個值,萬一將初始子彈數統一改爲 1000發呢?若是共享,那麼線程之間的併發修改會致使數據不許確.能不能構造這樣一個對象,將這個對象設置爲共享變量,統一設置初始值,可是每一個線程對這個值的修改都是互相獨立的.這個對象就是ThreadLocal框架

注意不能將其翻譯爲線程本地化或本地線程
英語恰當的名稱應該叫做:CopyValueIntoEveryThreaddom

示例代碼

實在難以理解的,能夠理解爲,JVM維護了一個Map ,每一個線程要用這個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 && 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 & (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<?>> {
    /** 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實例的變量只有 threadLocalHashCodenextHashCodeHASH_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開發手冊》

相關文章
相關標籤/搜索