一文帶你搞定ThreadLocal原理與使用

1、前言java

    成員變量會產生線程安全問題,局部變量不會有線程安全問題,由於局部變量是線程私有的,而成員變量是線程共享的。高併發的時候,調用一些公有的對象資源的時候,會有線程安全的問題。解決線程安全問題:(1)對成員變量進行加鎖,這樣的話其餘線程要使用的話,就必須等待,耗時;(2)把成員變量變成局部方法變量,很顯然,這樣的話不合理,設置爲局部變量,就不能在各個方法中使用了。這個時候可使用ThreadLocal來解決。ThreadLocal是併發場景下用來解決變量共享問題的類,它能使本來線程間共享的對象進行線程隔離,即一個對象只對一個線程可見,特別適用於各個線程依賴不一樣的變量值完成操做的場景。那麼ThreadLocal是怎麼作到的呢?算法

2、引用類型數組

    在分析ThreadLocal原理以前,先回顧下四種引用類型:強引用、軟引用、弱引用、虛引用。對象在堆上建立以後所持有的引用是一種變量類型,引用的可達性是JVM判斷可否被垃圾回收的基本條件。緩存

(1)強引用:平常所用的new一個對象,好比Object o = new Object();只要對象有強引用指向,而且GC Roots可達,那麼java內存回收時,即便內存耗盡,報出OutOfMemoryError,也不會回收該對象。安全

(2)軟引用:軟引用的生命週期比強引用短一些,經過SoftReference實現。在即將OOM以前,垃圾回收器會把軟引用指向的對象加入回收範圍,
以得到更多的內存空間。軟引用通常用來實現對內存敏感的緩存,若是有空閒內存就保留緩存,內存不足時就清理掉,這樣緩存的同時不會耗盡內存。併發

(3)弱引用:弱引用的生命週期比軟引用短,經過WeakReference實現。若是弱引用指向的對象只存在弱引用這條線路,則在下一次GC時會被回收。因爲GC時間的不肯定性,弱引用什麼時候被回收也具備不肯定性。ThreadLocal中的Entry就用到了弱引用。高併發

(4)虛引用:極弱的一種引用關係,經過PhantomReference實現。任什麼時候候均可以被GC回收,一個對象設置虛引用的目的是但願能在這個對象被回收時收到一個系統通知。注意,虛引用必須配合ReferenceQueue使用,在垃圾回收前會把虛引用加入到引用隊列中。this

3、ThreadLocal原理線程

    ThreadLocal提供了幾個核心方法:code

public T get()
public void set(T value)
public void remove()

其中:get()獲取當前線程的副本變量值,set()保存當前線程的副本變量值,remove移除當前線程的副本變量值。

一、get()

public T get() {
    // 獲取當前線程
    Thread t = Thread.currentThread();
    // 當前線程的threadLocals 
    ThreadLocalMap map = getMap(t);
    // threadLocals不爲空,就能夠在map中尋找到本地變量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // threadLocals爲空的話,就初始化當前線程的threadLocals變量
    return setInitialValue();
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    // 當前線程爲key,去找對應的線程變量,找對應的map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // map不爲null,直接添加本地變量,key爲當前線程,值爲添加的本地變量值
        map.set(this, value);
    else
        // map爲null,首次添加,須要先建立出對應的map,new了一個ThreadLocalMap
        createMap(t, value);
    return value;
}

二、set()

public void set(T value) {
    Thread t = Thread.currentThread();
    // 當前線程爲key,去找對應的線程變量,找對應的map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // map不爲null,直接添加本地變量,key爲當前線程,值爲添加的本地變量值
        map.set(this, value);
    else
        // map爲null,首次添加,須要先建立出對應的map,new了一個ThreadLocalMap
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    // 獲取線程本身的變量threadLocals,並綁定到當前調用線程的成員變量threadLocals上
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    // 不只建立了threadLocals,同時也將要添加的本地變量值添加到了threadLocals中
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

每一個線程都有一個ThreadLocalMap對象,每個新的線程都會實例化一個ThreadLocalMap,並賦值給線程的成員變量threadLocals。使用時若已經存在threadLocals則直接使用已經存在的對象。

三、remove()

public void remove() {
    // 獲取當前線程綁定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
    // map不爲null,移除當前線程中指定ThreadLocal實例的本地變量
     if (m != null)
         m.remove(this);
 }

四、ThreadLocalMap裏的Entry

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

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

ThreadLocalMap是ThreadLocal的內部類,沒有實現Map接口,用獨立的方式實現了Map的功能,其內部的Entry也獨立實現。Entry用來保存K-V結構數據,Entry中key只能是ThreadLocal對象。注意:Entry繼承自WeakReference,但只有Key是弱引用類型的,Value並不是弱引用。

4、要點

一、ThreadLocal不支持繼承性

同一個ThreadLocal變量在父線程中被設置值後,在子線程中是獲取不到的,由於threadLocals中爲當前調用線程對應的本地變量。若是子線程要訪問父線程的本地變量怎麼辦呢?可使用InheritableThreadLocal來解決,InheritableThreadLocal類繼承了ThreadLocal,而且重寫了childValue、getMap、createMap三個方法。

二、Hash衝突的解決

注意到ThreadLocalMap裏沒有next引用,那麼Hash衝突的話就不會像HashMap那樣使用鏈地址法,而是採用開放尋址法裏的線性探測法。即根據初始key的hashcode值肯定元素在table數組中的位置,若是發現這個位置上已經有其餘key值的元素被佔用,則利用固定的算法尋找必定步長的下個位置,依次判斷,直至找到可以存放的位置。ThreadLocalMap解決Hash衝突的方式就是簡單的步長加1或減1,尋找下一個相鄰的位置。

三、0x61c88647

int i = key.threadLocalHashCode & (len-1)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

Entry的索引i的位置是經過將threadLocalHashCode進行一個位運算(取模)獲得的。threadLocalHashCode的值爲何取0x61c88647呢?這點很是有趣,0x61c88647是斐波那契散列乘數,它的優勢是經過它散列(hash)出來的結果分佈會比較均勻,能夠很大程度上避免hash衝突。

四、髒數據與內存泄漏

因爲ThreadLocalMap的key是弱引用,而Value是強引用。這就致使了一個問題,ThreadLocal在沒有外部對象強引用時,發生GC時弱引用Key會被回收,而Value不會回收,若是建立ThreadLocal的線程一直持續運行,那麼複用線程(線程池)就會產生髒數據。這個Entry對象中的value有可能一直得不到回收,就會發生內存泄露。解決這兩個問題的方法是,每次用完ThreadLocal,要及時調用remove()來清理。

相關文章
相關標籤/搜索