ThreadLocal源碼解析,內存泄露以及傳遞性

我想ThreadLocal這東西,大家或多或少都瞭解過一點,我在接觸ThreadLocal的時候,覺得這東西很神奇,在網上看了很多博客,也看了一些書,總覺得有一個坎跨不過去,所以對ThreadLocal一直是一知半解的,好在這東西在實際開發中畢竟用的不多,所以也就得過且過了。當然我說的“用的不多”,只是對於普通的上層業務開發而言,其實在很多框架中,都用到了ThreadLocal,甚至有的還對ThreadLocal做了進一步的改進。但是ThreadLocal也算是併發編程的基礎,所以還真的有必要,也必須要好好研究下的。今天我們就來好好看看ThreadLocal。

ThreadLocal簡單應用

我們知道在多線程下,操作一個共享變量,很容易會發生矛盾,要解決這問題,最好的辦法當然是每個線程都擁有自己的變量,其他的線程無法訪問,所謂“沒有共享,就沒有傷害”。那麼如何做到呢?ThreadLocal就這樣華麗麗的登場了。

我們先來看看簡單的應用:

    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        threadLocal.set("Hello");
        System.out.println("當前線程是:" + Thread.currentThread().getName());
        System.out.println("在當前線程中獲取:" + threadLocal.get());
        new Thread(() -> System.out.println("現在線程是"+Thread.currentThread().getName()+"嘗試獲取:" + threadLocal.get())).start();
    }

運行結果:

當前線程是:main
在當前線程中獲取:Hello
現在線程是Thread-0嘗試獲取:null

運行結果很好理解,在主線程中往threadLocal 塞了一個值,只有在同一個線程下,纔可以獲得值,在其他線程就無法獲取值了。

嘗試自己寫一個ThreadLocal

在我們探究ThreadLocal之前,先讓我們思考一個問題,如果叫你來實現ThreadLocal,你會怎麼做?
ThreadLocal的目標就在於讓每個線程都有隻屬於自己的變量。最直接的辦法就是新建一個泛型類,在類中定義一個map,key是Long類型的,用來保存線程的id,value是T類型的,用來保存具體的數據。

  • set的時候,就獲取當前線程的id,把這個作爲key,往map裏面塞數據;

  • get的時候,還是獲取當前線程的id,把這個作爲key,然後從map中取出數據。

就像下面這個樣子:

public class ThreadLocalTest {
    public static void main(String[] args) {
        CodeBearThreadLocal threadLocal = new CodeBearThreadLocal();
        threadLocal.set("Hello");

        System.out.println("當前線程是:" + Thread.currentThread().getName());
        System.out.println("在當前線程中獲取:" + threadLocal.get());
        new Thread(() -> System.out.println("現在線程是" + Thread.currentThread().getName() + "嘗試獲取:" + threadLocal.get())).start();
    }
}

class CodeBearThreadLocal<T> {
    private ConcurrentHashMap<Long , T> hashMap = new ConcurrentHashMap<>();

    void set(T value) {
        hashMap.put(Thread.currentThread().getId(),value);
    }

    T get() {
       return hashMap.get(Thread.currentThread().getId());
    }
}

運行結果:

當前線程是:main
在當前線程中獲取:Hello
現在線程是Thread-0嘗試獲取:null

可以看到運行結果和“正版的ThreadLocal”是一模一樣的。

探究ThreadLocal

我們自己也寫了一個ThreadLocal,看上去一點問題也沒有,僅僅幾行代碼就把功能實現了,給自己鼓個掌。那正版的ThreadLocal是怎麼實現的呢?核心應該和我們寫的差不多吧。遺憾的是,正版的ThreadLocal和我們寫的可以說完全不一樣。

我們現在看看正版的ThreadLocal是怎麼做的。

set

    public void set(T value) {
        Thread t = Thread.currentThread();//獲取當前的線程
        ThreadLocalMap map = getMap(t);//獲取ThreadLocalMap 
        if (map != null)//如果map不爲null,調用set方法塞入值
            map.set(this, value);
        else
            createMap(t, value);//新建map
    }
  1. 獲取當前的線程賦值給t;
  2. 調用getMap方法,傳入t,也就是傳入當前線程,獲取ThreadLocalMap,賦值給map;
  3. 如果map不爲null,調用set方法塞入值;
  4. 如果map爲null,則調用createMap方法。

讓我們來看看getMap方法:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

getMap方法比較簡單,直接返回了傳進來的線程對象的threadLocals,說明threadLocals定義在Thread類裏面,是ThreadLocalMap 類型的,讓我們看看threadLocals的定義:

public class Thread implements Runnable{
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

看到這個定義,大家一定有點暈,我們是跟着ThreadLocal的set方法進來的,怎麼到了這裏又回到ThreadLocal了,大家彆着急,我們再來看看ThreadLocalMap是什麼鬼?



ThreadLocalMap是ThreadLocal的靜態內部類,我們的數據就是保存在ThreadLocalMap裏面,更詳細的說我們的數據就保存在ThreadLocal類中的ThreadLocalMap靜態內部類中的Entry[]裏面。

讓我們把關係理一理,確實有點混亂,Thread類裏面定義了ThreadLocal.ThreadLocalMap字段,ThreadLocalMap是TheadLocal的內部靜態類,其中的Entry[]是用來保存數據的。這就意味着,每一個Thread實例中的ThreadLocalMap都是獨一無二的,又不相互干擾。等等,這不就揭開了ThreadLocal的神祕面紗了嗎?原來ThreadLocal是這麼做到讓每個線程都有自己的變量的。

如果你還不清楚的話,沒關係,我們再來說的詳細點。在我們實現的ThreadLocal中,是利用map實現數據存儲的,key就是線程Id,你可以理解爲key就是Thread的實例,value就是我們需要保存的數據,當我們調用get方法的時候,就是利用線程Id,你可以理解爲利用Thread的實例去map中取出數據,這樣我們取出的數據就肯定是這個線程持有的。比如這個線程是A,你傳入了B線程的線程Id,也就是傳入了B線程的Thread的實例就肯定無法取出線程A所持有的數據,這點應該毫無疑問把。但是,在正版的ThreadLocal中,數據是直接存在Thread實例中的,這樣每個線程的數據就被天然的隔離了。

現在我們解決了一個問題,ThreadLocal是如何實現線程數據隔離的,但是還有一個問題,也就是我初學ThreadLocal看了很多博客,仍然百思不得其解的問題,既然數據是保存在ThreadLocalMap中的Entry[]的,那麼就代表可以保存多個數據,不然用一個普通的成員變量不就OK了嗎,爲什麼要用數組呢?但是ThreadLocal提供的set方法沒有重載啊,如果先set一個“hello”,又set一個“bye”,那麼“bye”肯定會把“hello”給覆蓋掉啊,又不像HashMap一樣,有key和value的概念。這個問題真的困擾我很久,後面終於知道了原因了,我們可以new多個ThreadLocal呀,就像這樣:

    public static void main(String[] args) {
        ThreadLocal threadLocal1 = new ThreadLocal();
        threadLocal1.set("Hello");

        ThreadLocal threadLocal2 = new ThreadLocal();
        threadLocal2.set("Bye");
    }

這樣一來,會發生什麼情況呢?再次放出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);
    }

threadLocal1,threadLocal2都調用了set方法,儘管threadLocal1和threadLocal2是不同的實例,但是它們在同一個線程啊,所以getMap獲取的ThreadLocalMap是同一個,這樣就變成了在同一個ThreadLocalMap保存了多個數據。

具體是怎麼保存數據的,這個代碼就比較複雜了,包括的細節太多了,我看的也不是很懂,只知道一個大概,我們先來看看Entry的定義把:

    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

Entry又是ThreadLocalMap的靜態內部類,裏面只有一個字段value,也就是說和HashMap是不同的,沒有鏈表的概念。

        private void set(ThreadLocal<?> key, Object value) {
            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();
        }
  1. 把table賦值給局部變量tab,這個table就是保存數據的字段,類型是Entry[];
  2. 獲取tab的長度賦值給len;
  3. 求出下標i;
  4. 一個for循環,先根據第三步求出的下標,從tab裏獲取指定下標的值e,如果e==null,就不會進入這個for循環,也就是如果當前的位置是空的,就直接進入第五步;如果當前的位置已經有數據了,判斷這個位置的ThreadLocal和我們即將要插入進去的是不是同一個,如果是的話,用新值替換掉;如果不是的話,則尋找下一個空位;
  5. 把創建出來的Entry實例放入tab。

其中的細節有點多,看的有點迷糊,但是最關鍵的應該還算是看懂了。

get

   public T get() {
        Thread t = Thread.currentThread();//獲取當前線程
        ThreadLocalMap map = getMap(t);//傳入當前線程,獲取當前線程的ThreadLocalMap 
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//傳入ThreadLocal實例,獲取Entry
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;//返回值
            }
        }
        return setInitialValue();
    }
  1. 獲取當前線程;
  2. 獲取當前線程的ThreadLocalMap;
  3. 傳入ThreadLocal實例,獲取Enrty;
  4. 返回值。
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
  1. 求出下標i;
  2. 根據下標i,從table中取出值,賦值給e;
  3. 如果e不爲空,並且e持有的ThreadLocal實例和傳進去的ThreadLocal實例是同一個,直接返回;
  4. 如果e爲空,或者e持有的ThreadLocal實例和傳進去的ThreadLocal實例不是同一個,則繼續往下找。

小總結

set方法和get方法都分析完畢了,我們來做一個小總結。我們在外面所使用的ThreadLocal更像是一個工具類,本身不保存任何數據,而真正的數據是保存在Thread實例中的,這樣就天然的完成了線程數據的隔離。最後送上一張圖,來幫助大家更好的理解ThreadLocal:

內存泄露

我們再來看看Entry的定義:

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

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

Entry繼承了WeakReference,關於WeakReference是什麼東西,不是本文的重點,大家可以自行查閱。WeakReference包裹了ThreadLocal,我們再來看Entry的構造方法,調用了super(k),傳入了我們傳進來的ThreadLocal實例,也就是ThreadLocal被保存到了WeakReference對象中。這就導致了一個問題,當ThreadLocal沒有強依賴,ThreadLocal會在下一次發生GC時被回收,key是被回收了,但是value卻沒有被回收呀,所以就出現了Entry[]存在key爲NULL,但是value不爲NULL的項的情況,要想回收的話,可以讓創建ThreadLocal的線程的生命週期結束。但是在實際的開發中,線程有極大可能是和程序同生共死的,只要程序不停止,線程就一直在蹦躂。所以我們在使用完ThreadLocal方法後,最好要手動調用remove方法,就像這樣:

    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal();
        try {
            threadLocal.set("Hello");
            threadLocal.get();
        } finally {
            threadLocal.remove();
        }
    }

別忘了,最好把remove方法放在finally中哦。

InheritableThreadLocal

我們還是來看博客一開頭的例子:

  public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        threadLocal.set("Hello");
        System.out.println("當前線程是:" + Thread.currentThread().getName());
        System.out.println("在當前線程中獲取:" + threadLocal.get());
        new Thread(() -> System.out.println("現在線程是" + Thread.currentThread().getName() + "嘗試獲取:" + threadLocal.get())).start();
    }

運行結果:

當前線程是:main
在當前線程中獲取:Hello
現在線程是Thread-0嘗試獲取:null

代碼後面new出來Thread是由主線程創建的,所以可以說這個線程是主線程的子線程,在主線程往ThreadLocal set的值,在子線程中獲取不到,這很好理解,因爲他們並不是同一個線程,但是我希望子線程能繼承主線程的ThreadLocal中的數據。InheritableThreadLocal出現了,完全可以滿足這樣的需求:

    public static void main(String[] args) {
        ThreadLocal threadLocal = new InheritableThreadLocal();
        threadLocal.set("Hello");
        System.out.println("當前線程是:" + Thread.currentThread().getName());
        System.out.println("在當前線程中獲取:" + threadLocal.get());
        new Thread(() -> System.out.println("現在線程是" + Thread.currentThread().getName() + "嘗試獲取:" + threadLocal.get())).start();
    }

運行結果:

當前線程是:main
在當前線程中獲取:Hello
現在線程是Thread-0嘗試獲取:null

這樣就讓子線程繼承了主線程的ThreadLocal的數據,說的更準確些,是子線程繼承了父線程的ThreadLocal的數據。

那到底是如何做到的呢?還是看代碼把。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal繼承了ThreadLocal,並且重寫了三個方法,當我們首次調用InheritableThreadLocal的set的時候,會調用InheritableThreadLocal的createMap方法,這就創建了ThreadLocalMap的實例,並且賦值給inheritableThreadLocals,這個inheritableThreadLocals定義在哪裏呢?和ThreadLocal的threadLocals一樣,也是定義在Thread類中。當我們再次調用set方法的時候,會調用InheritableThreadLocal的getMap方法,返回的也是inheritableThreadLocals,也就是把原先的threadLocals給替換掉了。

當我們創建一個線程,會調用Thread的構造方法:

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

init方法比較長,我只複製出和我們要探究的問題相關的代碼:

 Thread parent = currentThread();
 if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  1. 獲取當前線程,此時當前線程是父線程。
  2. 如果父線程的inheritableThreadLocals不爲空,就跑到if中去。當然這裏肯定是不爲空的,我們上面已經說了,調用InheritableThreadLocal中的set方法,直接操作的是inheritableThreadLocals,if中做了什麼,就是傳入了父線程的inheritableThreadLocals,創建了新的ThreadLocalMap,賦值給Thead實例的inheritableThreadLocals,這樣子線程就擁有了父線程的ThreadLocalMap,也就完成了ThreadLocal的繼承與傳遞。

這篇博客到這裏就結束了,東西還是挺多的,但是都是挺重要的,特別是ThreadLocal的原因和產生內存泄露的原因和避免的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章