我對ThreadLocal的一些理解 內存泄露啥的

1. 什麼是ThreadLocal

ThreadLocal提供了線程的局部變量,每個線程都可以通過set()和get()來對這個局部變量進行操作,但不會和其他線程的局部變量進行衝突,實現了線程的數據隔離~。

簡要言之:往ThreadLocal中填充的變量屬於當前線程,該變量對其他線程而言是隔離的。

ThreadLocal用在什麼地方?

討論ThreadLocal用在什麼地方前,我們先明確下,如果僅僅就一個線程,那麼都不用談ThreadLocal的,ThreadLocal是用在多線程的場景的!!!

ThreadLocal歸納下來就2類用途:

  • 保存線程上下文信息,在任意需要的地方可以獲取!!!

  • 線程安全的,避免某些情況需要考慮線程安全必須同步帶來的性能損失!!!

保存線程上下文信息,在任意需要的地方可以獲取!!!

由於ThreadLocal的特性,同一線程在某地方進行設置,在隨後的任意地方都可以獲取到。從而可以用來保存線程上下文信息。

常用的比如每個請求怎麼把一串後續關聯起來,就可以用ThreadLocal進行set,在後續的任意需要記錄日誌的方法裏面進行get獲取到請求id,從而把整個請求串起來。

還有比如Spring的事務管理,用ThreadLocal存儲Connection,ThreadLocal能夠實現當前線程的操作都是用同一個Connection,保證了事務!從而各個DAO可以獲取同一Connection,可以進行事務回滾,提交等操作。

同樣的,Hibernate對Connection的管理也是採用了相同的手法(使用ThreadLocal,當然了Hibernate的實現是更強大的)~

線程安全的,避免某些情況需要考慮線程安全必須同步帶來的性能損失!!!

每個線程往ThreadLocal中讀寫數據是線程隔離,互相之間不會影響的,所以ThreadLocal無法解決共享對象的更新問題!

由於不需要共享信息,自然就不存在競爭問題了,從而保證了某些情況下線程的安全,以及避免了某些情況需要考慮線程安全必須同步帶來的性能損失!!!

這類場景阿里規範裏面也提到了:

 

ThreadLocal一些細節!

ThreaLocal使用示例代碼:

public class ThreadLocalTest {

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

    public static void main(String[] args{

        new Thread(() -> {

            try {

                for (int i = 0; i < 100; i++) {

                    threadLocal.set(i);

                    System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get());

                    try {

                        Thread.sleep(200);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                }

            } finally {

                threadLocal.remove();

            }

        }, "threadLocal1").start();

 

        new Thread(() -> {

            try {

                for (int i = 0; i < 100; i++) {

                    System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get());

                    try {

                        Thread.sleep(200);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                }

            } finally {

                threadLocal.remove();

            }

        }, "threadLocal2").start();

    }

}

代碼運行結果:

從運行的結果我們可以看到threadLocal1進行set值對threadLocal2並沒有任何影響!

Thread、ThreadLocalMap、ThreadLocal總覽圖:

 

Thread類有屬性變量threadLocals (類型是ThreadLocal.ThreadLocalMap),也就是說每個線程有一個自己的ThreadLocalMap ,所以每個線程往這個ThreadLocal中讀寫隔離的,並且是互相不會影響的。

一個ThreadLocal只能存儲一個Object對象,如果需要存儲多個Object對象那麼就需要多個ThreadLocal!!!

如圖:

 

看到上面的幾個圖,大概思路應該都清晰了,我們Entry的key指向ThreadLocal用虛線表示弱引用 。

2. ThreadLocal實現的原理

首先,我們來看一下ThreadLocal的set()方法,因爲我們一般使用都是new完對象,就往裏邊set對象了

public void set(T value)

{

    // 得到當前線程對象

    Thread t = Thread.currentThread();

 

    // 這裏獲取ThreadLocalMap

    ThreadLocalMap map = getMap(t);

 

    // 如果map存在,則將當前線程對象t作爲key,要存儲的對象作爲value存到map裏面去

    if (map != null)

    map.set(this, value);

    else

    createMap(t, value);

}

 

上面有個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的一個內部類。用Entry類來進行存儲

我們的值都是存儲到這個Map上的,key是當前ThreadLocal對象

如果該Map不存在,則初始化一個。如果該Map存在,則從Thread中獲取

Thread維護了ThreadLocalMap變量 。

可以看出,ThreadLocalMap是在ThreadLocal中使用內部類來編寫的,但對象的引用是在Thread中

 

於是我們可以總結出:Thread爲每個線程維護了ThreadLocalMap這麼一個Map,而ThreadLocalMap的key是LocalThread對象本身,value則是要存儲的對象

 

有了上面的基礎,我們看get()方法就一點都不難理解了:

public T get()

{

    Thread t = Thread.currentThread();

    ThreadLocalMap map = getMap(t);

    if (map != null) {

        ThreadLocalMap.Entry e = map.getEntry(this);

        if (e != null) {

            @SuppressWarnings("unchecked")

            T result = (T)e.value;

            return result;

        }

    }

    return setInitialValue();

}

ThreadLocal原理總結

(1)每個Thread維護着一個ThreadLocalMap的引用

(2)ThreadLocalMap是ThreadLocal的內部類,用Entry來進行存儲

(3)調用ThreadLocal的set()方法時,實際上就是往ThreadLocalMap設置值,key是ThreadLocal對象,值是傳遞進來的對象

(4)調用ThreadLocal的get()方法時,實際上就是往ThreadLocalMap獲取值,key是ThreadLocal對象

(5)ThreadLocal本身並不存儲值,它只是作爲一個key來讓線程從ThreadLocalMap獲取value

正因爲這個原理,所以ThreadLocal能夠實現“數據隔離”,獲取當前線程的局部變量值,不受其他線程影響~

3. 避免內存泄露

我們來看一下ThreadLocal的對象關係引用圖:

 

 

我們Entry的key指向ThreadLocal虛線表示弱引用。

java對象的引用包括 :強引用,軟引用,弱引用,虛引用 。

因爲這裏涉及到弱引用,簡單說明下:

弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論內存是否充足,該對象僅僅被弱引用關聯,那麼就會被回收。

當僅僅只有ThreadLocalMap中的Entry的key指向ThreadLocal的時候,ThreadLocal會進行回收的!!!

ThreadLocal被垃圾回收後,在ThreadLocalMap裏對應的Entry的鍵值會變成null,但是Entry是強引用,那麼Entry裏面存儲的Object,並沒有辦法進行回收,所以ThreadLocalMap 做了一些額外的回收工作。

 

ThreadLocal內存泄漏的根源是:由於ThreadLocalMap的生命週期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因爲弱引用

 

想要避免內存泄露就要手動remove()掉

ThreadLocal的最佳實踐!

ThreadLocal被垃圾回收後,在ThreadLocalMap裏對應的Entry的鍵值會變成null,但是Entry是強引用,那麼Entry裏面存儲的Object,並沒有辦法進行回收,所以ThreadLocalMap 做了一些額外的回收工作。

 

備註:很多時候,我們都是用在線程池的場景,程序不停止,線程基本不會銷燬!!!

由於線程的生命週期很長,如果我們往ThreadLocal裏面set了很大很大的Object對象,雖然set、get等等方法在特定的條件會調用進行額外的清理,但是ThreadLocal被垃圾回收後,在ThreadLocalMap裏對應的Entry的鍵值會變成null,但是後續在也沒有操作set、get等方法了。

所以最佳實踐,應該在我們不使用的時候,主動調用remove方法進行清理。

 

這裏把ThreadLocal定義爲static還有一個好處就是,由於ThreadLocal有強引用在,那麼在ThreadLocalMap裏對應的Entry的鍵會永遠存在,那麼執行remove的時候就可以正確進行定位到並且刪除!!!

最佳實踐做法應該爲:

 

抽象爲:

try {

    // 其它業務邏輯

finally {

    threadLocal對象.remove();

}

 

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