再也不學Threadlocal了,看這一篇就忘不掉了(萬字總結)


為什么要學習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按照一個變量來理解,我們平常定義一個變量不就是這樣:

int a = 10;

所以對於ThreadLocal也是一樣,我們創建一個ThreadLocal就如同新創建一個變量一樣:

 private static ThreadLocal<Integer> local;

這個時候我們就定義了一個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進行了實例化:

local = new ThreadLocal();

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

ThreadLocal local = new ThreadLocal();

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

int a = 10;

就這樣,我們就給a賦值為10了,那么對於ThreadLocal而言,我們該怎么給它設置值呢?有如下的操作:

local.set();

就像我們上面代碼那樣:

local.set(a+10);

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

System.out.println(local.get()+Thread.currentThread().getName());

也就是通過:

local.get()

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

基本使用

也就是:

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

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

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

local.remove();

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

然后我們所展示的代碼還有這么一段:

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

這個就是開啟兩個線程。

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

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

接下來在第一個線程中實例化並且賦值:

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

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

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

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

local.remove();

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

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

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

local.get()

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

3、ThreadLocal的實現原理

源碼解讀get方法

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

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

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

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吧,那么這里判斷的就是空了,所以就會去走如下的語句:

return setInitialValue();

雖然這里我們並沒有這個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呢?我們就要看這個語句的執行了:

return setInitialValue();

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

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

T value = initialValue();

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

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

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

createMap(t, value);

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

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

這里的this其實是ThreadLocal的實例,也就是之前的local:

所以到了現在,這個get方法的我們分析的結果就是創建了一個ThreadLocalMap,然后往里面放了值,是一個key-value的形式,key就是我們的ThreadLocal實例。

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

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

源碼解讀set方法

對於get的方法實現肯定是一樣的,之所以這里得到值20,那是因為在當前線程執行了set方法:

local.set(a+10);

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

是不是很熟悉,也是先拿到當前線程,然后根據當前線程得到ThreadLocalMap,這里同樣之前沒有,所以需要重新創建,也就是去執行:

createMap(t, value);

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

熟悉不,又到了這里,創建了一個新的ThreadLocalMap來存放數據,this同樣也是ThreadLocal的實例,也就是local,這樣一來,key就對應我們的ThreadLocal實例,value就是傳過來的20了,另外我們大概知道,這么個鍵值對是放在ThreadLocalMap中的,然后我們通過當前線程可以得到這個ThreadLocalMap,再根據ThreadLocal這個實例就可以得到value的值,也就是20.

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

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

ThreadLocalMap的源碼解讀

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

經過我們上面的分析,我們知道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的源碼實現:

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

百度百科是這樣解釋的:

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

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

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

哈希表擴容

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

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

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

The next size value at which to resize.

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

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

它也有個注釋:

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的時候就需要對哈希表進行擴容了。

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

也即是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進行了實例化,並且在第一個線程總開始設置值,也就是調用set方法,我們進入到這個set方法看看:

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

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

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

table = new Entry[INITIAL_CAPACITY];

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

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

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

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

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

那為什么這樣寫呢?

firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

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

再接着看剩下的代碼:

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

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

table[i] = new Entry(firstKey, firstValue);

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

Entry長啥樣?

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

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

我們再來看這個Entry的實現,它是繼承了WeakReference<ThreadLocal>,這個是啥?WeakReference >是一個弱引用類型,簡單說,Entry本質上就是一個弱引用,因為是繼承WeakReference<ThreadLocal<?>>這個弱引用,所以它其實也是個弱引用,而Entry的實例說白了就是對ThreadLocal實例的一個弱引用,只不過Entry的設計上同時還保存了value值。

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

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

由於我們在第一次調用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的引用,那么就需要繼續往后挪一個位置來放入新的數據:

e = tab[i = nextIndex(i, len)])

當然,這個新的位置上依然要進入判斷,也是上面的情況,以此類推,直到找到一個位置要么為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函數的實現:

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

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

如何獲取Entry對象中的數據

那該如何獲取到Entry對象中的數據呢?也即是我們使用ThreadLocal的實例去調用get方法取值:

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

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

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

到了這里相信大家對ThreadLocalMap就有了一定的認識了,接下來我們繼續來聊聊ThreadLocal的內存泄露問題。

4、ThreadLocal的內存泄露

什么是內存泄漏和內存溢出

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

首先什么是內存泄漏

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

那什么是內存溢出呢?

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

好,了解了基本概念之后,我們再來看看T和read Local的內存泄漏,那為什么T和read Local會產生內存泄漏呢?我們再來看看這張圖:

經過我們上述的討論,我們大致知道了ThreadLocal其實本質上是在每個線程中單獨維護了一個ThreadLocalMap數據結構,這個ThreadLocalMap是每個線程獨有的,只有根據當前線程才能找到當前線程的這個ThreadLocalMap,這就實現了線程之前的隔離。

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

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

為什么會出現內存泄漏

那我們現在來看,為什么ThreadLocal會出現內存泄漏,我們之前也說過了,Entry對象持有的是鍵就是ThreadLocal實例的弱引用,弱引用有個什么特點呢?那就是在垃圾回收的時候會被回收掉,可以根據上圖想一下,圖中虛線就代表弱引用,如果這個ThreadLocal實例被回收掉,這個弱引用的鏈接也就斷開了,就像這樣:

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

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

如何解決內存泄漏

明白了如何產生的內存泄漏,也就知道了怎么解決,經過上面的分析,我們大致知道了在ThreadLocalMap中存在key為null的Entry對象,從而導致內存泄漏,那么只要把這些Entry都給刪除掉,也就解決了內存泄漏。

我們每次使用ThreadLocal就會隨線程產生一個ThreadLocalMap,里面維護Entry對象,我們對Entry進行存取值,那么如果我們每次使用完ThreadLocal之后就把對應的Entry給刪除掉,這樣不就解決了內粗泄漏嘛,那怎么做呢?

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

這個就是根據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進行清理。

6、關於原創

本文原創作者:ithuangqing
轉載請注明出處,微信公眾號開白請聯系我微信H653836923

▼ 慶哥有一個夢想,寫一些能讓小白看得懂學得會的技術教程,幫助初學者更快的入門與進階,於是乎,在編碼之外開啟了逐夢之旅!關注公眾號,后台回復“慶哥”,2019最新java自學資源立馬送上!

在這里插入圖片描述

長按二維碼識別關注!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM