【Java併發編程】——ThreadLocal的深入解析與使用,以及內存泄漏問題

目錄

什麼是ThreadLocal?

如何使用ThreadLocal?

內存泄漏

爲什麼會導致內存泄漏?

如何避免內存泄漏?

源碼解析:

void set()方法

T get()方法

void remove()方法

InheritableThreadLocal類


什麼是ThreadLocal?

ThreadLocal,意爲線程本地變量

ThreadLocal是JDK包提供的,它提供了線程本地變量,也就是如果你創建了一個ThreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的一個本地副本。當多個線程操作這個變量時,實際操作的是自己本地內存裏面的變量,從而避免了線程安全問題。

如何使用ThreadLocal?

我們先看下面這段代碼

public class ThreadTest{

    // 創建ThreadLocal變量
    static ThreadLocal<String> localVariable = new ThreadLocal<>();

    static void print(String str) {
        // 打印當前線程副本變量的值
        System.out.println(str + ":" + localVariable.get());
       
    }
    public static void main(String[] args) {
        // 創建線程1
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                localVariable.set("this is threadOne local variable");
                print("threadOne");
                System.out.println("threadOne remove after:" + localVariable.get());
            }
        });
        // 創建線程2
        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                localVariable.set("this is threadTwo local variable");
                print("threadTwo");
                System.out.println("threadTwo remove after:" + localVariable.get());
            }
        });
        // 開啓線程
        threadOne.start();
        threadTwo.start();
    }
}

 

通過運行結果,我們可以看出,兩個線程通過調用print方法都打印出了當前線程副本變量的值,然後又在run()方法中獲取到了當前線程的副本變量的值。

接下來我們在print()方法中添加一條語句,也就是輸出完之後就將副本變量刪除掉

 // 清除當前線程副本變量
localVariable.remove();

 然後再來看一下運行結果

 

我們可以發現,在run()方法中再次獲取localVariable變量的值時,已經獲取不到了,因爲那個副本變量已經在當前線程中刪除掉了。

 

內存泄漏

爲什麼會導致內存泄漏?

ThreadLocalMap 使用 ThreadLocal 的弱引用作爲key,如果一個 ThreadLocal 沒有外部強引用來引用它,那麼系統 GC 的時候,這個 ThreadLocal 就會被回收,由此一來,ThreadLocalMap 中就會出現 key 爲 null 的 Entry,也就沒有辦法再去訪問對應的 value,如果當前線程再遲遲不結束的話,這些 key 爲 null 的 Entry 的 value 就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永遠無法回收,然後下次再使用的時候則會繼續創建新的,越來越多,最終造成內存泄漏。

如何避免內存泄漏?

爲了避免內存泄漏,當不需要使用本地變量時可以通過調用ThreadLocal變量的remove方法,從當前線程的threadLocals裏面刪除該本地變量,如果一直不刪除,最終會導致內存泄漏。 

源碼解析:

Thread類中有一個threadLocals和inheritableThreadLocals,它們都是ThreadLocalMap類型的變量,而ThreadLocalMap是一個特殊的HashMap

void set()方法

(在當前線程中,設置一個變量的副本)

先獲取當前線程,然後根據當前線程作爲key,獲取到對應線程的變量,如果map不爲空就爲這個map賦值,否則就創建一個新的變量,併爲其賦值

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

getMap()源碼如下:

就是獲取對應線程自己的變量threadLocals

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

createMap()源碼如下:

創建一個新的threadLocals變量

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

T get()方法

(獲取當前線程中副本變量的值)

首先獲取當前線程,然後根據當前線程作爲key,獲取到對應線程的ThreadLocals變量,如果變量不爲空,則返回當前線程綁定的本地變量,否則執行setInitialValue()

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

getEntry源碼如下:

官方解釋:此方法本身只處理快速路徑::直接命中已存在的鍵,爲了最大限度地提高直接命中的性能而設計的...

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

setInitialValue()源碼如下:

先設置一個value,初始化爲null,然後獲取當前線程,在獲取當前線程的threadLocals變量,如果變量不爲空,則設置變量的值爲value(也就是null),否則就新建一個threadLocals變量。

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

void remove()方法

(刪除當前線程中的副本變量)

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

InheritableThreadLocal類

ThreadLocal不支持繼承性:也就是說,同一個ThreadLocal變量在父線程中被設置值後,在子線程中是獲取不到的,那麼怎樣才能在子線程也能獲取到呢?這時就需要使用InheritableThreadLocal類了。

  • InheritableThreadLocal繼承自ThreadLocal,其提供了一個特性,就是讓子線程可以訪問在父線程中設置的本地變量。
  • 當父線程創建子線程時,構造函數會把父線程中inheritableThreadLocals變量裏面的本地變量複製一份保存到子線程的inheritableThreadLocals變量裏面。

 例:

public class ThreadTest{

    // 創建ThreadLocal變量
    static ThreadLocal<String> localVariable = new ThreadLocal<>();

    public static void main(String[] args) {
        // 在主線程中設置變量的值
        localVariable.set("Hello Java");
        // 創建線程1
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                // 子線程輸出變量的值
                System.out.println("threadOne:" + localVariable.get());
            }
        });
        // 開啓線程
        threadOne.start();
        // 主線程輸出變量的值
        System.out.println("main:" + localVariable.get());
    }
}

輸出結果如下 :

main:Hello Java
threadOne:null

我們可以看到,主線程中設置了變量的值,在子線程中是獲取不到的

然後我們修改上面代碼中創建變量的方法:

// 創建ThreadLocal變量
static ThreadLocal<String> localVariable = new InheritableThreadLocal<>();

接下來再運行一遍,看一下結果:

main:Hello Java
threadOne:Hello Java

可以看到,子線程中也獲取到了變量的值

因爲在父線程創建子線程時,構造函數會把父線程中inheritableThreadLocals變量裏面的副本變量複製一份保存到子線程的inheritableThreadLocals變量裏面。

注意:ThreadLocal使用完後,一定要刪除,否則會造成內存泄漏,平時的時候可能察覺不到,但當數據量很大的時候,就會出現內存泄漏的情況,而且也會影響業務邏輯。

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