簡談ThreadLocal

1、ThreadLocal 線程私有的。java

爲何說ThreadLocal是線程私有的?  上源碼 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);
    }

從源碼得知。咱們在往 ThreadLocal 中存數據時。 它首先 獲取到當前線程。並從當前線程中拿到 ThreadLocalMap 。再往裏存放數據。jvm

接下來咱們再來看ThreadLocalMap 中  key ,value 究竟又存放的是什麼。顯然 ThreadLocalMap 中 key ->ThreadLocal  ;value ->咱們set 進去的 value。this

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 .set()  方法時,每次都是獲取當前線程,再完成數據存儲動做的。那麼,它天然 具有了線程隔離性。由某一個線程所持有。因此ThreadLocal是線程私有的。spa

接下來,咱們用代碼來證實.net

/**
 *  首先 我先初始化一個 threadLocal 。、
 *  再主線程main 中 新啓一個線程 thread,並在該線程中完成 set操做。
 *  由此 當前應用存在兩個線程 一個是 main ,一個是 thread
 *  因爲threadLocal 是線程私有的。因此 主線程 main 獲取的值爲 null
 */
public class Test {

    public static void main(String[] args) {

        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        Thread thread = new Thread(()->{
            System.out.println(String.format("往 線程%s 中 存入 1 ",Thread.currentThread().getName()));
            threadLocal.set(1);
            try{
                Thread.sleep(1000);
                System.out.println(String.format("線程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
            }catch(Exception ex){
                ex.printStackTrace();
            }

        });
        thread.start();

        try{
            // 讓 主線程睡一下子。確保 cpu 已執行 thread 中的 run()方法
            Thread.sleep(500);
        }catch(Exception ex){
            ex.printStackTrace();
        }

        System.out.println(String.format("線程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
    }
}

驗證完畢。讓咱們繼續來看下源碼。線程

1.1 首先,咱們來看一下 Thread 類。java源碼中包含一個 ThreadLocalMap 的成員變量 threadLocalsdebug

/*java源碼中包含一個 ThreadLocalMap 的成員變量 threadLocals */


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

從上圖咱們能夠看出ThreadLocalMap 是 ThreadLocal 中的  靜態內部類。code

1.2 接着咱們來看下 ThreadLocal 中的源碼。orm

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  中  也包含一個 靜態內部類 Entry 繼承自 WeakReference<ThreadLocal<?>> 同時擁有 一個ThreadLocal 的引用。和 value。

那麼問題來了。此處爲何要用 弱引用?

首先。咱們再來複習一下,弱引用的特色。  當一個對象沒有被強引用存在時。 弱引用 將被jvm忽視。直接gc掉。想一下,若是不是 弱引用。而是強引用,會有什麼問題。假使 Entry 未繼承弱引用,則 entry 對 ThreadLocal  強引用。 則 當  ThreadLocal 被複製爲null時 意味着,ThreadLocal 已經沒有用了。 但 entry 對 ThreadLocal  的 強引用。致使 ThreadLocal  沒有辦法被回收。 會形成內存泄漏。 因此 這裏被繼承自 弱引用。

驗證來了。上代碼,看看 ThreadLocal 被置爲空後。 是否被gc了。

/**
 * 首先 我建立了兩個 ThreadLocal 對象 分別是 threadLocal,threadLocal_2
 * 分別往 主線程 main 中 存放數字 1,2
 * 而後將 threadLocal 置爲空。 而後再 手動 gc 。
 * 分別再gc 前 和 gc 後 查看 當前 線程中的 ThreadLocalMap。
 * 因爲獲取 ThreadLocalMap 訪問級別爲default  因此我這裏將用 debug的方式進行查看。
 */
public class Test1 {

    public static void main(String[] args) {

        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        ThreadLocal<Integer> threadLocal_2 = new ThreadLocal<>();

        threadLocal.set(1);

        threadLocal_2.set(2);

        System.out.println(String.format("線程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));

        threadLocal_2.get();

        threadLocal = null;

        System.gc();
        threadLocal_2.get();



    }
}

 

未gc前。debug 看到的 狀況。

 

gc 後。 debug 看到的狀況。

終上所訴。驗證了 弱引用的實際效果。 和 爲何不用強引用 的緣由。

這裏,咱們發現了。 弱引用後, ThreadLocalMap 中對應的 entry 的 referent (指向 threadLocal的引用)確實被gc了。 

繼續看源碼。 referent 是哪裏來的。 entry 繼承自 WeakReference<ThreadLocal<?>> 。而 WeakReference 又繼承自 抽象類 Reference<T>。

private T referent;         /* Treated specially by GC */

    volatile ReferenceQueue<? super T> queue;

    /* When active:   NULL
     *     pending:   this
     *    Enqueued:   next reference in queue (or this if last)
     *    Inactive:   this
     */
    @SuppressWarnings("rawtypes")
    volatile Reference next;

好,瞭解到這裏。 咱們不難發現。 雖然被gc了。可是 仍是存在一個問題,ThreadLocalMap  -》entry -》key -》 referent (threadLocal)已然爲null了。可是卻仍然存在ThreadLocalMap中,佔用的部分的內存。 應用不可能經過某個引用再次拿到 entry 中的value了。 那不就是內存泄漏了嗎?

好問題! 這裏編寫人員,也想到了該問題。因此,他們的處理邏輯是:再調用 set,remove 方法時,調用方法 expungeStaleEntry 將 鍵爲null 的對象remove掉。

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;


// 將entry的value賦值爲null,這樣方便GC時將真正value佔用的內存給釋放出來;將entry賦值爲null,size減1,這樣這個slot就又能夠從新存放新的entry了
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;     

    
    // Rehash until we encounter null
    Entry e;
    int i;

   // 從staleSlot後一個index開始向後遍歷,直到遇到爲null的entry
    for (i = nextIndex(staleSlot, len); 
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
// 若是entry的key爲null,則清除掉該entry
        if (k == null) {    
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {   // key的hash值不等於目前的index,說明該entry是由於有哈希衝突致使向後移動到當前index位置的
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)      // 對該entry,從新進行hash並解決衝突
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
 // 返回通過整理後的,位於staleSlot位置後的第一個爲null的entry的index值
    return i;  
}

到這裏,這樣處理,就萬事大吉了嗎? 遠遠沒有這麼簡單。 新問題又來了。 假使 set完後,就再也沒有調用 set 和 remove 方法。那 不仍是內存泄漏了嗎?

因此再使用 ThreadLocal 時,要養成一個好習慣,ThreadLocal 再沒有用時,就將 ThreadLocal 置爲null。以避免出現內存泄漏。

最後,咱們來分析一個問題。ThreadLocal  是線程私有的。若是是多線程中(多線程中的線程是可複用的)使用了 ThreadLocal 會有什麼問題?

答案也很簡單。若是沒有及時清理 ThreadLocal 除內存泄露外,還可能引起數據問題。 話很少說, 上代碼。

/**
 * 前後建立6個任務,前3個線程寫數據,後3個線程讀取數據。
 *
 * 發現threadLocal 裏的 值被取出了。
 *
 * 假使有個業務場景是 往當前線程 存放 用戶名(採用ThreadLocal來存儲) 。
 * 先進行非空判斷,再進行 存儲。 結果,悲劇了。 上個線程的 ThreadLocal 並無清理。致使 不爲空。
 * 結果上線文中存儲的用戶名就亂套了。可能就張冠李戴了
 */
public class Test2 {

    public static void main(String[] args) {

        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

        ThreadLocal<String> threadLocal = new ThreadLocal<String>();

        for(int i=0;i<3;i++){
            fixedThreadPool.execute(()->{
                try {
                    threadLocal.set("ckr");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }


        try{
            Thread.sleep(2000);
        }catch(Exception ex){

        }

        for(int i=0;i<3;i++){
            fixedThreadPool.execute(()->{
                try {
                    System.out.println(threadLocal.get());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }


        try{
            Thread.sleep(2000);
        }catch(Exception ex){

        }

        fixedThreadPool.shutdown();
    }
}

以上。關於ThreadLocal的一些問題,咱們都瞭解了,再次,特別強調,ThreadLocal 有風險。須要謹慎使用

關於 java 中的四種引用強,軟,弱,虛。 請查看上篇博客: http://www.javashuo.com/article/p-czdxdtqo-nx.html

相關文章
相關標籤/搜索