ThreadLocal實現原理

注:在《透徹理解Spring事務設計思想之手寫實現》中,已經向你們揭示了Spring就是利用ThreadLocal來實現一個線程中的Connection是同一個,從而保證了事務。本篇博客將帶你們來深刻分析ThreadLocal的實現原理。java

1、ThreadLocal定義

      ThreadLocal,顧名思義,它不是一個線程,而是線程的一個本地化對象。當工做於多線程中的對象使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程分配一個獨立的變量副本。因此每個線程均可以獨立地改變本身的副本,而不會影響其餘線程所對應的副本。從線程的角度看,這個變量就像是線程的本地變量,這也是類名中「Local」所要表達的意思。 
      線程局部變量並非Java的新發明,不少語言(如IBM XL、FORTRAN)在語法層面就提供線程局部變量。在Java中沒有提供語言級支持,而以一種變通的方法,經過ThreadLocal的類提供支持。因此,在Java中編寫線程局部變量的代碼相對來講要笨拙一些,這也是爲何線程局部變量沒有在Java開發者中獲得很好普及的緣由。 數組

      歸納起來講,對於多線程資源共享的問題,同步機制採用了「以時間換空間」的方式:訪問串行化,對象共享化。而ThreadLocal採用了「以空間換時間」的方式:訪問並行化,對象獨享化。前者僅提供一份變量,讓不一樣的線程排隊訪問,然後者爲每個線程都提供了一份變量,所以能夠同時訪問而互不影響。 安全

原理圖:數據結構

 

ThreadLocal的實現是這樣的:每一個Thread 維護一個 ThreadLocalMap 映射表,這個映射表的 key 是 ThreadLocal實例自己,value 是真正須要存儲的 Object。
也就是說 ThreadLocal 自己並不存儲值,它只是做爲一個 key 來讓線程從 ThreadLocalMap 獲取 value。值得注意的是圖中的虛線,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用做爲 Key 的,弱引用的對象在 GC 時會被回收。多線程

2、TreadLocal 源碼實現

1.ThreadLocal get實現

/*
* Returns the value in the current thread's copy of this
* thread-local variable.  If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
           @SuppressWarnings("unchecked")
           T result = (T)e.value;
           return result;
        }
    }
    return setInitialValue();
}

/*
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param  t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/*
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
private T setInitialValue() {
	T value = initialValue();
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		map.set(this, value);
	} else {
		createMap(t, value);
    }
	return value;
}

2.ThreadLocal中set實現

/*
* Sets the current thread's copy of this thread-local variable
* to the specified value.  Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
*        this thread-local.
*/
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
	    map.set(this, value);
    } else {
	    createMap(t, value);
    }
}

3.Thread中ThreadLocal定義

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

4.ThreadLocal中ThreadLocalMap實現

static class ThreadLocalMap {
    /*
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
     static class Entry extends WeakReference<ThreadLocal<?>> {
         /** The value associated with this ThreadLocal. */
         Object value;
         Entry(ThreadLocal<?> k, Object v) {
             super(k);
             value = v;
         }
     }

     /*
      * The initial capacity -- MUST be a power of two.
      */
     private static final int INITIAL_CAPACITY = 16;
     /*
      * The table, resized as necessary.
      * table.length MUST always be a power of two.
      */
     private Entry[] table;

     ...
}

    從名字上看,能夠猜到它也是一個相似HashMap的數據結構,可是在ThreadLocal中,並沒實現Map接口。在ThreadLoalMap中,也是初始化一個大小16的Entry數組,Entry對象用來保存每個key-value鍵值對,只不過這裏的key永遠都是ThreadLocal對象,是否是很神奇,經過ThreadLocal對象的set方法,結果把ThreadLocal對象本身當作key,放進了ThreadLoalMap中。ide

   

hash衝突

沒有鏈表結構,那發生hash衝突了怎麼辦?函數

先看看ThreadLoalMap中插入一個key-value的實現this

/*
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal<?> key, Object value) {
    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

每一個ThreadLocal對象都有一個hash值 threadLocalHashCode,每初始化一個ThreadLocal對象,hash值就增長一個固定的大小 0x61c88647spa

在插入過程當中,根據ThreadLocal對象的hash值,定位到table中的位置i,過程以下: 一、若是當前位置是空的,那麼正好,就初始化一個Entry對象放在位置i上; 二、不巧,位置i已經有Entry對象了,若是這個Entry對象的key正好是即將設置的key,那麼從新設置Entry中的value; 三、很不巧,位置i的Entry對象,和即將設置的key不要緊,那麼只能找下一個空位置;線程

這樣的話,在get的時候,也會根據ThreadLocal對象的hash值,定位到table中的位置,而後判斷該位置Entry對象中的key是否和get的key一致,若是不一致,就判斷下一個位置

能夠發現,set和get若是衝突嚴重的話,效率很低,由於ThreadLoalMap是Thread的一個屬性,因此即便在本身的代碼中控制了設置的元素個數,但仍是不能控制其它代碼的行爲。

總結

 能夠從ThreadLocal的get函數中看出來,其中getmap函數是用t做爲參數,這裏t就是當前執行的線程。
從而得知,get函數就是從當前線程的threadlocalmap中取出當前線程對應的變量的副本【注意,變量是保存在線程中的,而不是保存在ThreadLocal變量中】。當前線程中,有一個變量引用名字是threadLocals,這個引用是在ThreadLocal類中createmap函數內初始化的。每一個線程都有一個這樣的threadLocals引用的ThreadLocalMap,以ThreadLocal和ThreadLocal對象聲明的變量類型做爲參數。這樣,咱們所使用的ThreadLocal變量的實際數據,經過get函數取值的時候,就是經過取出Thread中threadLocals引用的map,而後從這個map中根據當前threadLocal做爲參數,取出數據。

3、ThreadLocal 內存泄漏

1.ThreadLocal爲何會內存泄漏?

      ThreadLocalMap使用ThreadLocal的弱引用做爲key,若是一個ThreadLocal沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,若是當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠沒法回收,形成內存泄漏。
其實,ThreadLocalMap的設計中已經考慮到這種狀況,也加上了一些防禦措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap裏全部key爲null的value。
可是這些被動的預防措施並不能保證不會內存泄漏:
      使用static的ThreadLocal,延長了ThreadLocal的生命週期,可能致使的內存泄漏(參考ThreadLocal 內存泄露的實例分析)。
      分配使用了ThreadLocal又再也不調用get(),set(),remove()方法,那麼就會致使內存泄漏。

2.爲何使用弱引用?

     從表面上看內存泄漏的根源在於使用了弱引用。網上的文章大多着重分析ThreadLocal使用了弱引用會致使內存泄漏,可是另外一個問題也一樣值得思考:爲何使用弱引用而不是強引用?
咱們先來看看官方文檔的說法:
    To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
     爲了應對很是大和長時間的用途,哈希表使用弱引用的 key。
下面咱們分兩種狀況討論:
key 使用強引用:引用的ThreadLocal的對象被回收了,可是ThreadLocalMap還持有ThreadLocal的強引用,若是沒有手動刪除,ThreadLocal不會被回收,致使Entry內存泄漏。
key 使用弱引用:引用的ThreadLocal的對象被回收了,因爲ThreadLocalMap持有ThreadLocal的弱引用,即便沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
      比較兩種狀況,咱們能夠發現:因爲ThreadLocalMap的生命週期跟Thread同樣長,若是都沒有手動刪除對應key,都會致使內存泄漏,可是使用弱引用能夠多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
     所以,ThreadLocal內存泄漏的根源是:因爲ThreadLocalMap的生命週期跟Thread同樣長,若是沒有手動刪除對應key就會致使內存泄漏,而不是由於弱引用。

3.怎麼避免內存泄漏?

     每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
在使用線程池的狀況下,沒有及時清理ThreadLocal,不只是內存泄漏的問題,更嚴重的是可能致使業務邏輯出現問題。因此,使用ThreadLocal就跟加鎖完要解鎖同樣,用完就清理。

4、ThreadLocal在Spring中應用

     ThreadLocal在Spring中發揮着重要的做用,Spring使用ThreadLocal解決線程安全問題,在管理request做用域的Bean、事務管理、任務調度、AOP等模塊都出現了它們的身影,起着舉足輕重的做用。要想了解Spring事務管理的底層技術,ThreadLocal是必須攻克的山頭堡。

相關文章
相關標籤/搜索