天天看點

阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合

1.4 ThreadLocalRandom 可将其執行個體設定到靜态變量,在多線程下重用嗎?

current()的時候初始化一個初始化種子到線程,每次nextseed再使用之前的種子生成新的種子:

UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);      

如果你通過主線程調用一次current生成一個ThreadLocalRandom執行個體儲存,那麼其它線程來擷取種子的時候必然取不到初始種子,必須是每一個線程自己用的時候初始化一個種子到線程。

可以在nextSeed設定一個斷點看看:

UNSAFE.getLong(Thread.currentThread(),SEED);      

2 記憶體洩漏

在源碼注釋中提示使用static關鍵字來修飾

ThreadLocal

.

在此場景下,寄希望于

ThreadLocal

對象失去引用後,觸發弱引用機制來回收

Entry

Value

就不現實了.

在上例中,如果不進行

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()方法的源碼

阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合
阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合
阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合
阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合

可見,每個線程中都有一個

ThreadLocalMap

  • 執行set時,其值是儲存在目前線程的

    threadLocals

    變量
  • 執行get時,從目前線程的

    threadLocals

    變量擷取

是以線上程A中set的值,是線程B永遠得不到的

即使線上程B中重新set,也不會影響A中的值;

保證了線程之間不會互相幹擾.

追尋本質 - 結構

從名字上看猜它類似HashMap,但在

ThreadLocal

中,并無實作Map接口

  • ThreadLoalMap

    中,也是初始化一個大小為16的Entry數組
  • 阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合
  • Entry節點對象用來儲存每一個key-value鍵值對
  • 阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合
  • 這裡的key 恒為 ThreadLocal;

通過

ThreadLocal

set()

,把

ThreadLocal

對象自身當做key,放進

ThreadLoalMap

阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合
  • ThreadLoalMap

    Entry

    繼承

    WeakReference

    和HashMap很不同,

    Entry

    中沒有

    next

    字段,是以不存在連結清單情形.

hash沖突

無連結清單,那發生hash沖突時何解?

先看看

ThreadLoalMap

插入一個 key/value 的實作

阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合

每個

ThreadLocal

對象都有一個hash值 - 

threadLocalHashCode

阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合

每初始化一個

ThreadLocal

對象,hash值就增加一個固定大小

阿裡三面:說說線程封閉與ThreadLocal的關系(下)What is ThreadLocal追尋本質 - 結構hash沖突記憶體洩露題外小話Hibernate中典型的 ThreadLocal 應用ThreadLocal的應用場合

在插入過程中,根據

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執行個體的變量隻有 threadLocalHashCode

nextHashCode 和HASH_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()方法同樣看了代碼也就明白了.