深刻了解Threadlocal

前言

想必你們都對Threadlocal很熟悉吧,今天咱們就一塊兒來深刻學習一下。Threadlocal我更傾向於將其翻譯成線程局部變量。它有什麼用處呢?Threadlocal對象一般用於防止對可變的單實例變量或全局變量進行共享。在spring中,經過將事務上下文保存在靜態的threadlocal中,當框架代碼須要判斷當前運行的是哪個事務時,只須要從ThreadLocal對象中獲取事務上下文,這種機制很方便,避免了在每一個方法都要傳遞上下文信息。html

一個小例子

衆所周知,SimpleDateFormat不是一個線程安全的類,在多線程環境下使用同一個實例是不正確的。經過將SimpleDateFormat保存在Threadlocal中,每一個線程都會擁有屬於本身的SimpleDateFormat,代碼以下所示:面試

public class DateFormatUtil {

    public static ThreadLocal<SimpleDateFormat> dataFormatlocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat();
        }
    };

    public static SimpleDateFormat getInstance() {
        return dataFormatlocal.get();
    }

}

而後你就能夠安心地調用了,不用考慮線程安全問題。固然你也能夠在每一個線程內部調用new SimpleDateFormat(),但現實狀況是大部分開發可能並不知道它是線程不安全的,而且將其封裝成一個單例的工具類。至於如何使用,全憑各位讀者愛好,這個例子主要用來演示Threadloca的基本用法。spring

腦洞大開---本身設計個threadlocal

經過以前的演示,咱們已經大體知道threadlocal怎麼用,而且清楚threadlocal保證了每一個線程都有一個局部變量副本。若是以此爲需求,讓咱們本身去設計,會是什麼樣子的呢?api

static Map<String,Object> threadlocal = new HashMap<String,Object>();

這就是我設計的....好吧(╯▽╰),原諒我水平有限....在這個hashMap中,threadId爲key,Object爲value,也是能夠實現Threadlocal的。那麼咱們如今要來考慮幾個問題。jdk是這樣設計的嗎?這樣設計很low,可是low在哪了?
答:大師們固然不是這樣設計了。若是這樣設計,每一個線程的變量都會永久的保存在hashMap中,存在內存泄漏。安全

好奇寶寶---jdk是如何實現threadlocal的

low的寫法咱們已經見過了,如今咱們一塊兒來分下jdk是如何設計的,本文引用jdk1.8。
讓咱們看下threadlocal的結構圖:
圖片描述數據結構


類核心方法set、get、initialValue、setInitialValue、remove,後面主要圍繞着這幾個方法介紹。多線程


類核心變量threadLocalHashCode,nextHashCode,HASH_INCREMENT,其中nextHashCode和HASH_INCREMENT都是靜態的,因此對於一個threadlocal對象,成員變量只有一個threadLocalHashCode,這是一個自定義的hash函數,主要爲了減小散列桶的衝突(不是本文重點,好奇的能夠看下這篇博客https://www.cnblogs.com/ilell...)。框架


那到底Threadlocal將變量存到哪了呢?眼尖的同窗確定已經發現了ThreadLocalMap,再來看下ThreadlocalMap的結構:ide

clipboard.png

相信讀過hashMap源碼同窗的必定會以爲很是眼熟,ThreadlocalMap的主體也是Entry[],有resize()和rehash(),區別僅僅就是在於沒有使用鏈表結構和紅黑樹來處理散列衝突了。Entry的結構咱們也頗有必要了解一下:函數

clipboard.png

Entry繼承了一個弱引用,這裏簡單介紹下弱引用的知識。弱引用是用來描述非必需對象的,被弱引用關聯的對象只能生存到下次垃圾收集發生以前。不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。不少面試官都喜歡問Threadlocal是怎麼發生內存泄漏的,其實就是在問這個,這個須要咱們對Threadlocal有一個總體的瞭解才能明白,因此放在最後講。


當咱們看完Threadlocal的結構了,咱們發現ThreadlocalMap是用來存儲的數據結構,那它是怎麼和線程關聯起來的呢?這裏咱們開始研究Threadlocal.set()方法.

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

步驟1:獲取當前線程
步驟2:經過當前線程獲取ThreadLocalMap
步驟3:若是map不爲null,將當前線程和value放到map中,不然建立一個map。
如何和線程綁定的玄機就在getMap(t)中。

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

看到這裏,咱們就知道了原來Threadlocal有一個內部類ThreadlocalMap,對於任何一個線程都會有惟一的一個ThreadlocalMap來對應,而這個map實際並不存儲在Threadlocal中,而是存在Thread當中,只不過由Threadlocal暴露了一套api來維護Thread的ThreadLocalMap。這樣設計的好處就是,當線程死掉以後,ThreadLocalMap沒有強引用,方便收集器回收。


繼續來看get()

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

若是前面的流程看懂了,這就很簡單了。當ThreadLocalMap爲null或者Entry爲null的時候將會調用setInitialValue();

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;
    }

流程也很簡單,調用initialValue()初始化一個值,獲取當前線程的ThreadLocalMap,後面的流程以前都已經介紹過了,這裏再也不重複,咱們主要來看下initialValue()

protected T initialValue() {
        return null;
    }

請注意,protected修飾,return null;這是一個初始化值得方法,也就意味着若是業務容許的話,須要咱們本身實現initialValue();


ThreadLocal的remove實現

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocalMap的remove實現

private void remove(ThreadLocal key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

都很是簡單,就留給你們本身看了~。~.

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;
            }
        }
        .....
        .....
}

而後套用網上的一張圖(實線表示強引用,虛線表示虛引用)
圖片描述
ThreadLocalMap使用ThreadLocal的弱引用做爲key,若是一個ThreadLocal沒有外部強引用引用他,那麼系統gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,若是當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:ThreadLocal Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠沒法回收,形成內存泄露。其實jdk已經考慮到了這種狀況,ThreadLocalMap的genEntry函數或者set函數會去遍歷將key爲null的給移除掉,但這明顯不是全部狀況都成立的,因此須要調用者本身去調用remove函數,手動刪除掉須要的threadlocal,防止內存泄露。而後jdk並不建議在棧內聲明threadlocal,而是建議將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,因爲一直存在ThreadLocal的強引用,因此ThreadLocal也就不會被回收,也就能保證任什麼時候候都能根據ThreadLocal的弱引用訪問到Entry的value值,而後remove它,防止內存泄露。
放圖證實這是jdk說的~~

clipboard.png

總結

再不睡覺,就天明瞭..

相關文章
相關標籤/搜索