天天看點

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

為什麼要學習ThreadLocal呢?因為面試官經常問,而且線上程中使用它可以給我們提供一個線程内的本地局部變量,這樣就可以減少在一個線程中因為多函數之間的操作導緻共享變量傳值的複雜性,說白了,我們使用ThreadLocal可以做到在一個線程内随時随地的取用,而且與其他的線程互不幹擾。

在一些特殊的情景中,應用ThreadLocal會帶來極大的便利,不過很多人卻搞不懂Threadlocal到底是個啥?在我們的面試中也經常會被問到Threadlocal,是以基于我們的實際應用以及應對面試,我們都有必要好好的學習下Threadlocal。

今天,我們就來完完整整的學習下Threadlocal,争取以後再也不學了,因為看完今天這篇文章,你就對Threadlocal忘不了了!

1、什麼是Threadlocal?

首先,我們既然要學習Threadlocal,那麼我們先要知道它是個啥?我們從名字來看,Threadlocal意思就是線程本地的意思,我們這個屬于猜想,并不權威,那麼要想知道他是個啥,最好的辦法就是看看官方是怎麼定義它的,我們看看ThreadLocal的源碼(基于jdk1.8)中對這個類的介紹:

This class provides thread-local variables. These variables differ from* their normal counterparts in that each thread that accesses one (via its* {@code get} or {@code set} method) has its own, independently initialized* copy of the variable. {@code ThreadLocal} instances are typically private* static fields in classes that wish to associate state with a thread (e.g.,* a user ID or Transaction ID).

這是在jdk1.8中對ThreadLocal這個類給的注釋,我們簡單翻譯一下就是:

此類提供線程局部變量。這些變量與正常變量不同,因為每個通路一個線程(通過其{@code get}或{@code set}方法)的線程都有其自己的,獨立初始化的變量副本。 {@code ThreadLocal}執行個體通常是希望将狀态與線程相關聯的類中的私有靜态字段(例如使用者ID或交易ID)。

什麼意思呢?我們大緻能夠看明白,說是TreadLocal可以給我們提供一個線程内的局部變量,而且這個變量與一般的變量還不同,它是每個線程獨有的,與其他線程互不幹擾的。

現在我們簡單的對ThreadLocal有了認識,下面我們就直接上代碼,看看它的一個實際應用例子。

2、如何使用ThreadLocal?

看代碼

先來看一段代碼:

public class Test {
    private static int a = 10;
    private static ThreadLocal<Integer> local;
    public static void main(String[] args) {

        Thread A = new Thread(new ThreadA());
        A.start();
        ThreadB B = new ThreadB();
        B.start();

    }

    static class ThreadA implements Runnable{
        @Override
        public void run() {
            local = new ThreadLocal();
            local.set(a+10);
            System.out.println(local.get()+Thread.currentThread().getName());
            local.remove();
            System.out.println(local.get()+Thread.currentThread().getName());
        }
    }

    static class ThreadB extends Thread{
        @Override
        public void run() {
             System.out.println(local.get()+Thread.currentThread().getName());

        }
    }
}
           

我們之前就知道,ThreadLocal是為我們提供一個線程局部變量的,那我們測試的方法就是建立兩個線程,使用ThreadLocal去存取值,看看兩個線程之間會不會互相影響,上面的這段代碼我們來簡單分析一下,首先是兩個變量:

private static int a = 10;
    private static ThreadLocal<Integer> local;
           

注意看,這裡就使用到了ThreadLocal了,使用方法和普通的變量幾乎是一樣的,我們這個時候就可以把ThreadLocal按照一個變量來了解,我們平常定義一個變量不就是這樣:

是以對于ThreadLocal也是一樣,我們建立一個ThreadLocal就如同新建立一個變量一樣:

這個時候我們就定義了一個ThreadLocal,注意這個時候隻是定義而沒有進行初始化指派,并不像int a = 10那樣已經指派為10了,現在的ThreadLocal還隻是定義好而已,我們繼續看下面的代碼:

static class ThreadA implements Runnable{
        @Override
        public void run() {
            local = new ThreadLocal();
            local.set(a+10);
            System.out.println(local.get()+Thread.currentThread().getName());
            local.remove();
            System.out.println(local.get()+Thread.currentThread().getName());
        }
    }

    static class ThreadB extends Thread{
        @Override
        public void run() {
               System.out.println(local.get()+Thread.currentThread().getName());

        }
    }
           

這裡是定義了兩個線程,注意看了,在第一個線程中的run方法内,我們對ThreadLocal進行了執行個體化:

到這裡,我們就完整的建立了一個ThreadLocal,也就是下面這樣:

我們之前說可以把ThreadLocal看做是一個變量,像普通的變量,比如下面這樣:

就這樣,我們就給a指派為10了,那麼對于ThreadLocal而言,我們該怎麼給它設定值呢?有如下的操作:

就像我們上面代碼那樣:

這樣我們就給ThreadLocal給指派了,那麼怎麼拿到這個值呢?如同上面代碼所示:

也就是通過:

至此,我們就知道ThreadLocal最基本的使用了。

基本使用

也就是:

ThreadLocal local = new ThreadLocal();
local.set(a+10);
local.get()

           

到這裡我們有沒有覺得它像是一個map,也是key-value的形式來存取值的呢?

另外在上面的代碼中還有如下的一句代碼:

這個也好了解,是删除,删除啥呢?我們先留個疑問,接下來的文章會慢慢說,看到最後,你就明白了。

然後我們所展示的代碼還有這麼一段:

Thread A = new Thread(new ThreadA());
A.start();
ThreadB B = new ThreadB();
B.start();

           

這個就是開啟兩個線程。

至此,我們所展示的代碼就簡單的分析了一下,重點看了ThreadLocal是個簡單的使用。

那麼這段代碼會輸出什麼結果呢?在看輸出之前,我們需要強調一點,ThreadLocal可以提供線程内的局部變量,各個線程之間互不幹擾。那我們在思考上面所展示的代碼。首先是定義ThreadLocal:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

接下來在第一個線程中執行個體化并且指派:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

然後我們看在第二個線程中:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

大眼一看,貌似覺得應該還是20,畢竟是同一個local啊,而且local在之前已經指派了等于20,這裡隻不過在另外一個線程中再次去取這個值,我們來看看輸出結果:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

看到結果我們知道了,雖然在第一個線程中ThreadLocal被執行個體化且指派了,而且正确取值20,但是在另一個線程中去取值的話為空,我們再來稍微改變下代碼:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

哦,似乎明白了,對于ThreadLocal而言,每個線程都是有一個單獨存在的,相當于一個副本,線程之間互不影響,這裡面還有一個null是因為調用了:

這相當于把值删除了,自然為空,想一想,上述的結果不就說明了ThreadLocal的作用嗎?提供線程局部變量,每個線程都有自己的一份,線程之間沒有影響。

可能有的人不明白了,這裡的local不都是這個嗎?

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

難道不是同一個?按理說是一個啊,在另外一個線程中應該取值是一樣的啊,怎麼會是空呢?而且在另外一個線程中也隻是調用了這個簡單的get方法啊:

哦,我知道了,這個可能就是get的問題,在不同的線程之間get的實作是不同的,那它的底層是怎麼實作的呢?

3、ThreadLocal的實作原理

源碼解讀get方法

好了,肯定有人迫不及待的想看看這個get是怎麼實作的,為什麼會出現上述的結果,那我們就一起來看看這個get的底層源碼:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

這個就是get方法的實作了,可能我們猛一看并不能完全看明白每個細節,但是大緻意思已經很清楚了,接下來我們來簡單的分析一下,對了我們現在要解決的問題是為什麼在另一個線程中調用get方法之後得到的值是null,也就是這個:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

我們首先來看這兩句代碼:

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);

           

首先是擷取目前線程,然後根據目前線程得到一個ThreadLocalMap,這個ThreadLocalMap是個啥,我們暫時還不知道,解下來就進行了如下判斷:

if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }

           

也就是在判斷根據目前線程得到的ThreadLocalMap是否為空,我們想想,我們就是直接調用get就來到了這裡,好像并滅有什麼地方去建立了這個ThreadLocalMap吧,那麼這裡判斷的就是空了,是以就會去走如下的語句:

雖然這裡我們并沒有這個Map,但是我們看如果有map的話是怎麼執行呢?我們仔細看看這段代碼:

ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }

           

這不就是在傳回我們需要的值嘛?這個值是從這個ThreadLocalMap中拿到的,哦,到了這裡似乎知道了,為啥在另一個線程中調用get會得到null,那是因為值被放到了一個叫做ThreadLocalMap的東西裡面了,而它又是根據目前線程建立的,也就是說每個線程的ThreadLocalMap是不同的,在目前線程中并沒有建立,是以也就沒值。

為什麼是null?

嗯嗯,這個想法貌似很對,不過又有個問題,為啥會是null呢?我們就要看這個語句的執行了:

從這個方法的名字可以猜想,這應該是初始化操作的。我們看看這方法是如何實作的:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

在這個方法之中,首先會執行如下語句:

我們看看這個方法的實作:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

原來就傳回一個null啊,那麼上面的value就是null了,然後我們再看下面的語句,是不是覺得很熟悉:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

我們知道,這裡map是沒有的,是以會走else,也就是回去執行如下的方法:

對了,這裡的value是個null,而t就是目前線程啦,我們繼續看看這個方法的實作:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

哦,看到這裡似乎就知道,在這個方法中就建立了一個ThreadLocalMap,我們之前看源碼覺得資料是被放到了這個ThreadLocalMap中了,那麼現在這裡已經建立了一個ThreadLocalMap,那麼資料是哪個呢?看方法實作,應該就是那個firstValue了,我們知道這個值就是之前傳過來的value,也就是null,這相當于一個value值,那麼這裡的key呢?是不是就是這個this,那麼這個this指的誰呢?

這裡的this其實是ThreadLocal的執行個體,也就是之前的local:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

是以到了現在,這個get方法的我們分析的結果就是建立了一個ThreadLocalMap,然後往裡面放了值,是一個key-value的形式,key就是我們的ThreadLocal執行個體。

然後我們再看執行完createMap之後的操作,就是直接傳回value了,也就是一個null,是以現在我們明白了為什麼這裡調用get是個null。

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

看到這裡,這個get是明白怎麼回事了,那麼在第一個線程中的get也是這樣的嗎?

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

源碼解讀set方法

對于get的方法實作肯定是一樣的,之是以這裡得到值20,那是因為在目前線程執行了set方法:

local.set(a+10);

           

根據我們之前對get的分析,這裡我們應該可以猜想到,set方法也建立了一個ThreadLocalMap并且把值放了進去,是以執行get能得到值,我們一起來看下set的實作:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

是不是很熟悉,也是先拿到目前線程,然後根據目前線程得到ThreadLocalMap,這裡同樣之前沒有,是以需要重新建立,也就是去執行:

但是這裡的value就不是null了,而是傳過來的20,我們接着看這個方法的實作:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

熟悉不,又到了這裡,建立了一個新的ThreadLocalMap來存放資料,this同樣也是ThreadLocal的執行個體,也就是local,這樣一來,key就對應我們的ThreadLocal執行個體,value就是傳過來的20了,另外我們大概知道,這麼個鍵值對是放在ThreadLocalMap中的,然後我們通過目前線程可以得到這個ThreadLocalMap,再根據ThreadLocal這個執行個體就可以得到value的值,也就是20.

我們接下來看這個線程中的get的執行:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

因為我們在set的時候就建立了ThreadLocalMap,是以這裡就不會再去建立了,因為已經有map了,是以會直接執行:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

ThreadLocalMap的源碼解讀

這裡其實就牽涉到ThreadLocalMap的内部實作了,看到這裡我們需要來借助一張圖以便加深了解,就是下面的這張圖:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

經過我們上面的分析,我們知道ThreadLocal的設定值的方式是key-value的形式,也知道了這裡的key其實就是ThreadLocal的執行個體,value就是要設定的值。

這裡我們看下ThreadLocalMap,它其實是一個資料結構,就是用來存放我們的值的,而且它也是ThreadLocal的一個核心,我們通過上面這張圖,首先要知道的一點就是:

ThreadLocalMap中存儲的是Entry對象,Entry對象中存放的是key和value。

至于為什麼是這樣的,我們一步步的來分析ThreadLocalMap!

ThreadLocalMap中的Entry

在ThreadLocalMap中其實是維護了一張哈希表,這個表裡面就是Entry對象,而每一個Entry對象簡單來說就是存放了我們的key和value值。

那麼這個是如何實作的呢?首先我們來想,Entry對象是存放在ThreadLocalMap中,那麼對于TreadLocalMap而言就需要一個什麼來存放這個Entry對象,我們可以想成一個容器,也就是說ThreadLocalMap需要有一個容器來存放Entry對象,我們來看ThreadLocalMap的源碼實作:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

在ThreadLocalMap中定義了一個Entry數組table,這個就是存放Entry的一個容器,在這裡我們首先需要知道一個概念,那就是什麼是哈希表?

百度百科是這樣解釋的:

散清單(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行通路的資料結構。也就是說,它通過把關鍵碼值映射到表中一個位置來通路記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散清單。

給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能得到包含該關鍵字的記錄在表中的位址,則稱表M為哈希(Hash)表,函數f(key)為哈希(Hash) 函數。

上面也提到過,ThreadLocalMap其實就是維護了一張哈希表,也即是一個數組,這個表裡面存儲的就是我們的Entry對象,其實就是它:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

哈希表擴容

涉及到哈希表,必然會涉及到另外一個概念,那就是增長因子,那什麼是增長因子呢?

簡單來說,這是一個值,當表裡面存儲的對象達到了表的總容量的某個百分比的時候,這張表就該擴容了,那麼這個百分比就是增長因子,我們看ThreadLocalMap中的增長因子:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

從這些代碼我們可以了解到,ThreadLocalMap中定義了一個threshold屬性,這個屬性上面有個介紹,也就是:

The next size value at which to resize.

翻譯過來就是:要調整大小的下一個大小值。

什麼意思呢?也就是說當哈希表中存儲的對象的數量超過了這個值的時候,哈希表就需要擴容,那麼這個值具體是多大呢?下面有個方法:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

它也有個注釋:

Set the resize threshold to maintain at worst a 2/3 load factor.

翻譯過來就是:設定調整大小門檻值以保持最壞的2/3負載系數。

意思就是設定這個增長因子為總容量的2/3,這個增長因子就是threshold。也就是當哈希表的容量達到了總容量的2/3的時候就需要對哈希表進行擴容了。

Entry對象是如何存儲資料的

到這裡我們就知道了,ThreadLocalMap維護了一個哈希表,表裡面存儲的就是Entry對象,當哈希表的目前容量達到了總容量的2/3的時候就需要對哈希表進行擴容了。

那麼可能有人會問了,初始容量是多少啊?這個在源碼中也有展現:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

也即是16,那麼對于資料而言,它又是怎樣被放到哈希表中的呢?接下來我們就來看看ThreadLocalMap中設定值的方法:

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設定值得時候開始一步步的進入到這段源代碼,首先就是這段代碼:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

這是在第一個線程中,我們對ThreadLocal進行了執行個體化,并且在第一個線程總開始設定值,也就是調用set方法,我們進入到這個set方法看看:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

我們之前就分析過了,這裡沒有map,會去建立,我們進入到createMap中看看:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

這裡建立了ThredLocalMap,調用了它的構造方法,我們進入看看:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

這段代碼就需要好好解讀了,首先是它:

這個table沒有忘記是啥吧,就是之前定義的Entry數組,就是這個:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

這裡的INITIAL_CAPACITY就是初始化容量16,是以這裡就建構了一個容量為16的Entry數組,這個數組就可以用來存放我們的資料,具體怎麼存放,我們接着往下看:

這裡是為了得到一個下表,因為哈希表是依靠一個索引去存取值得,是以會根據這個下标值去決定把資料存放到哪個位置,簡單點就是把資料放到數組中的哪個位置,這個就是數組下标,那這個threadLocalHashCode是個啥呢?我們看看:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

它是通過這個nextHashCode方法得到的,這個nextHashCode也有一系列的操作,反正最終目的就是為了得到一個索引值,或者是下标值,來決定這個資料存放到哪個位置。

那為什麼這樣寫呢?

這是拿得到的threadLocalHashCode對Entry數組的總容量減去一的值做取餘操作,目的就是為了得到的下标值始終都在數組内,防止下标越界的。

再接着看剩下的代碼:

table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);

           

拿到下标值之後就得到了一個位置就是table[i],然後就是把一個具體的Entry對象放進去了,剩下的就是設定目前表中有幾條資料,也就是有幾個Entry對象了,然後根據初始容量設定增長因子,我們重點來看看這段代碼:

table[i]也就是Entry數組中的一個确切的位置,是要放入一個Entry對象的,這裡就new了一個新的Entry對象,并把key和value傳入了進去,我們看看這個Entry的構造方法以及這個Entry類的實作。

Entry長啥樣?

我們先來看看它的這個構造函數:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

這其實也是Entry類的源碼,其中有一個構造函數,傳入key和value,在Entry中還定義了一個Object類型的value變量,把随構造函數傳入進來的value值指派給這個Object類型的value變量,這樣就将value儲存在了Entry中了。

我們再來看這個Entry的實作,它是繼承了WeakReference<ThreadLocal<?>>,這個是啥?WeakReference

到這裡,我們就知道了這個Entry是如何儲存鍵值對的了,也知道Entry其實就是個弱引用。

對了,你要知道上述我們的分析是針對ThreadLocal第一次調用set方法的時候因為沒有map需要建立map走得上述方法,如果是再次調用則會走map中的set方法,我們具體看源碼:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

由于我們在第一次調用set方法時已經建立了map,那麼再次set的時候就會主席那個map的set方法,我們來看看map的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();
        }

           

這就是ThreadLocalMap中通過set方式設定值的源碼實作,第一次調用是通過構造函數的形式設定資料,我們現在來看看這個set方式的資料設定。

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

           

首先是拿到之前建立的Entry數組,這裡是tab,然後也是計算出一個新的下标值來存放新資料,接下來就是這段代碼:

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

           

首先要知道這是一個for循環,根據一個下标值得到一個新的Entry對象,然後進入循環條件 也即是這個Entry對象不為null,然後執行循環體,循環體中有兩個判斷,還有一個根據目前Entry對象得到ThreadLocal的引用,也即是Key,不過這裡定義為k。

現在我們要知道,我們是要往Entry數組中放入一個新的Entry對象,具體放到哪裡由i這個下标值确定,具體的位置就是table[i],是以會出現的情況就有這個位置原本就有一個Entry對象或者為null,于是如果原本就有的話而且引用的是同一個ThreadLocal的話,那麼就把值給覆寫掉:

if (k == key) {
                    e.value = value;
                    return;
                }

           

如果是這個位置是null的話,我們就放入新的值:

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

           

當然,也會出現的情況就是這個位置不為null,而且也不是同一個ThreadLocal的引用,那麼就需要繼續往後挪一個位置來放入新的資料:

當然,這個新的位置上依然要進入判斷,也是上面的情況,以此類推,直到找到一個位置要麼為null,要麼是同一個ThreadLocal的引用,隻有這樣才能放入新的資料。

我們接着來看下面的代碼,執行完上面的判斷之後會執行如下的代碼:

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

           

這個就是建立具體的Entry對象,因為Entry數組多了一個Entry對象,是以總條目需要加一,而這個if判斷則是為了看看目前存儲的對象個數是否達到了增長因子,也就是判斷下是否需要擴容,如果需要擴容了該怎麼辦呢?這個時候要依靠的就是這個rehash函數了。

rehash函數是如何實作重新擴充并重新計算位置的

如果達到了增長因子,那就需要重新擴充,而且還需要将所有的對象重新計算位置,我們來看rehash函數的實作:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

我們看到在if判斷中判斷的名額是增長因子的3/4,這是怎麼回事,之前不是說增長因子是2/3嘛?超過這個值才需要擴容,這怎麼變成了增長因子的3/4才開始擴容呢?我們之前說過,ThreadLocalMap中存儲的是Entry對象,Entry本質上是個ThreadLocal的弱引用,是以它随時都有可能被回收掉,這樣就會出現key值為null的Entry對象,這些都是用不到的,需要删除掉來騰出空間,這樣一來,實際上存儲的對象個數就減少了,是以後面的判斷就是增長因子的3/4,而不是增長因子2/3了。

而expungeStaleEntries();就是做這樣的清理工作的,把占坑的Entry統統删除掉。

如何擷取Entry對象中的資料

那該如何擷取到Entry對象中的資料呢?也即是我們使用ThreadLocal的執行個體去調用get方法取值:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

因為已經有map了,是以我們直接就調用map的getEntry方法,我們看看這個方法的實作:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

這段代碼還是比較簡單的,首先根據哈希碼值算出下标i,然後就确定了這個Entry的位置,如果這個位置不為空而且對用的ThreadLocal的弱引用也是我們需要的這個,那麼就傳回這個Entry對象中儲存的value值。

當然,如果對應的位置為空的話,我們就需要getEntryAfterMiss函數來進行進一步的判斷了。

到了這裡相信大家對ThreadLocalMap就有了一定的認識了,接下來我們繼續來聊聊ThreadLocal的記憶體洩露問題。

4、ThreadLocal的記憶體洩露

什麼是記憶體洩漏和記憶體溢出

我們在講ThreadLocal的記憶體洩漏之前,首先要搞清楚什麼是記憶體洩漏,那要說起記憶體洩漏,肯定還有個概念需要說,那就是記憶體溢出,這兩者是個啥呢?

首先什麼是記憶體洩漏:

說的簡單點那就是因為操作不當或者一些錯誤導緻沒有能釋放掉已經不再使用的記憶體,這就是記憶體洩漏,也就是說,有些記憶體已經不會再使用了,但是卻沒有給它釋放掉,這就一直占用着記憶體空間,進而導緻了記憶體洩漏。

那什麼是記憶體溢出呢?

這個簡單點說就是記憶體不夠用了,我運作一個程式比如說需要50M的記憶體,但是現在記憶體就剩下20M了,那程式運作就會發生記憶體溢出,也就是告訴你記憶體不夠用,這時候程式就無法運作了。

好,了解了基本概念之後,我們再來看看T和read Local的記憶體洩漏,那為什麼T和read Local會産生記憶體洩漏呢?我們再來看看這張圖:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

經過我們上述的讨論,我們大緻知道了ThreadLocal其實本質上是在每個線程中單獨維護了一個ThreadLocalMap資料結構,這個ThreadLocalMap是每個線程獨有的,隻有根據目前線程才能找到目前線程的這個ThreadLocalMap,這就實作了線程之前的隔離。

我們看上面那張圖,每個線程根據找到自己維護的ThreadLocalMap,然後可以操作這個資料結構,往裡面存取資料,而ThreadLocalMap中維護的就是一個Entry數組,每個Entry對象就是我們存放的資料,它是個key-value的形式,key就是ThreadLocal執行個體的弱引用,value就是我們要存放的資料,也就是一個ThreadLocal的執行個體會對用一個資料,形成一個鍵值對。

如果有兩個線程,持有同一個ThreaLocal的執行個體,這樣的情況也就是Entry對象持有的ThreadLocal的弱引用是一樣的,但是兩個線程的ThreadLocalMap是不同的,記住一點,那就是ThreadLocalMap是每個線程單獨維護的。

為什麼會出現記憶體洩漏

那我們現在來看,為什麼ThreadLocal會出現記憶體洩漏,我們之前也說過了,Entry對象持有的是鍵就是ThreadLocal執行個體的弱引用,弱引用有個什麼特點呢?那就是在垃圾回收的時候會被回收掉,可以根據上圖想一下,圖中虛線就代表弱引用,如果這個ThreadLocal執行個體被回收掉,這個弱引用的連結也就斷開了,就像這樣:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

那麼這樣在Entry對象中的key就變成了null,是以這個Entry對象就沒有被引用,因為key變成看null,就取不到這個value值了,再加上如果這個目前線程遲遲沒有結束,ThreadLocalMap的生命周期就跟線程一樣,這樣就會存在一個強引用鍊,是以這個時候,key為null的這個Entry就造成了記憶體洩漏。

因為它沒有用了,但是還沒有被釋放。

如何解決記憶體洩漏

明白了如何産生的記憶體洩漏,也就知道了怎麼解決,經過上面的分析,我們大緻知道了在ThreadLocalMap中存在key為null的Entry對象,進而導緻記憶體洩漏,那麼隻要把這些Entry都給删除掉,也就解決了記憶體洩漏。

我們每次使用ThreadLocal就會随線程産生一個ThreadLocalMap,裡面維護Entry對象,我們對Entry進行存取值,那麼如果我們每次使用完ThreadLocal之後就把對應的Entry給删除掉,這樣不就解決了内粗洩漏嘛,那怎麼做呢?

在ThreadLocal中提供了一個remove方法:

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

這個就是根據key删除掉對應的Entry,如此一來,我們就解決了記憶體洩漏問題,因為可能出現記憶體洩漏的Entry,在我們使用完之後就立馬删除了。

是以對于ThreadLocal而言,就應該像使用鎖一樣,加鎖之後要記得解鎖,也就是調用它的remove方法,用完就清理。

5、總結

至此,我們已經對ThreadLocal做了一個較為全面和深入的分析,大家應該也對它有了更深的印象,下面針對本文來做一個簡單的總結:

1、ThreadLocal是用來提供線程局部變量的,線上程内可以随時随地的存取資料,而且線程之間是互不幹擾的。

2、ThreadLocal實際上是在每個線程内部維護了一個ThreadLocalMap,這個ThreadLocalMap是每個線程獨有的,裡面存儲的是Entry對象,Entry對象實際上是個ThreadLocal的執行個體的弱引用,同時還儲存了value值,也就是說Entry存儲的是鍵值對的形式的值,key就是ThreadLocal執行個體本身,value則是要存儲的資料。

3、TreadLocal的核心是底層維護的ThreadLocalMap,它的底層是一個自定義的哈希表,增長因子是2/3,增長因子也可以叫做是一個門檻值,底層定義為threshold,當哈希表容量大于或等于門檻值的3/4的時候就開始擴容底層的哈希表數組table。

4、ThreaLocalMap中存儲的核心元素是Entry,Entry是一個弱引用,是以在垃圾回收的時候,ThreadLocal如果沒有外部的強引用,它會被回收掉,這樣就會産生key為null的Entry了,這樣也就産生了記憶體洩漏。

5、在ThreadLocal的get(),set()和remove()的時候都會清除ThreadLocalMap中key為null的Entry,如果我們不手動清除,就會造成記憶體洩漏,最佳做法是使用ThreadLocal就像使用鎖一樣,加鎖之後要解鎖,也就是用完就使用remove進行清理。

感謝閱讀

大學的時候選擇了自學Java,工作了發現吃了計算機基礎不好的虧,學曆不行這是沒辦法的事,隻能後天彌補,于是在編碼之外開啟了自己的逆襲之路,不斷的學習Java核心知識,深入的研習計算機基礎知識,所有心得全部書寫成文,整理成有目錄的PDF,持續原創,PDF在公衆号持續更新,如果你也不甘平庸,那就與我一起在編碼之外,不斷成長吧!

其實這裡不僅有技術,更有那些技術之外的東西,比如,如何做一個精緻的程式員,而不是“屌絲”,程式員本身就是高貴的一種存在啊,難道不是嗎?

非常歡迎你的加入,未來的日子,編碼之外,有你有我,一起做一個人不傻,錢很多,活得久的快樂的程式員吧!

回複關鍵字“PDF”,擷取技術文章合集,已整理好,帶有目錄,歡迎一起交流技術!

另外回複“慶哥”,看慶哥給你準備的驚喜大禮包,隻給首次關注的你哦!

任何問題,可以加慶哥微信:H653836923,另外,我有個交流群,我會***不定期在群裡分享學習資源,不定時福利***,感興趣的可以說下我邀請你!

對了,如果你是個Java小白的話,也可以加我微信,我相信你在學習的過程中一定遇到不少問題,或許我可以幫助你,畢竟我也是過來人了!

聽說阿裡百度這樣的大公司,面試經常拿ThreadLocal考驗求職者?(萬字總結)1、什麼是Threadlocal?2、如何使用ThreadLocal?3、ThreadLocal的實作原理4、ThreadLocal的記憶體洩露5、總結

感謝各位大大的閱讀🥰

繼續閱讀