平時常說的ThreadLocal,今天就徹底解決它

前言

ThreadLocal是多線程處理中非常重要的一個工具,比如數據庫連接池存放Connection、存放本地參數等作用,面試也經常會問到它的應用及原理,本文就將從外到內地學習一下ThreadLocal。

一、瞭解ThreadLocal的作用

ThreadLocal顧名思義是線程私有的局部變量存儲容器,可以理解成每個線程都有自己專屬的存儲容器,它用來存儲線程私有變量,其實它只是一個外殼,內部真正存取是一個Map,後面會仔細講解。每個線程可以通過set()get()存取變量,多線程間無法訪問各自的局部變量,相當於在每個線程間建立了一個隔板。只要線程處於活動狀態,它所對應的ThreadLocal實例就是可訪問的,線程被終止後,它的所有實例將被垃圾收集。總之記住一句話:ThreadLocal存儲的變量屬於當前線程

二、ThreadLocal簡單使用

話不多說先看一下ThreadLocal的一個簡單案例

Code:

public class Test implements Runnable {
    private static AtomicInteger counter = new AtomicInteger(100);
    private static ThreadLocal<String> threadInfo = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "[" + Thread.currentThread().getName() + "," + counter.getAndIncrement() + "]";
        }
    };

    @Override
    public void run() {
        System.out.println("threadInfo value:" + threadInfo.get());
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Test());
        Thread thread2 = new Thread(new Test());

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("threadInfo value in main:" + threadInfo.get());
    }
}

Output:

threadInfo value:[Thread-0,100]
threadInfo value:[Thread-1,101]
threadInfo value in main:[main,102]

上述代碼中我用ThreadLocal來存儲線程的信息,其格式爲[線程名,線程ID],定義的變量是靜態的。從運行結果可以看出來每個線程包括主線程訪問到的threadInfo獲取到的值都是不一樣的,而且存放的信息就是本線程的信息,也應證了上面那句話ThreadLocal存儲的變量屬於當前線程

三、ThreadLocal原理

3.1 ThreadLocal的存取過程

解析原理先從源碼開始,首先看一下ThreadLocal.set()方法

//  ThreadLocal中set方法
public void set(T value) {
    //  獲取當前線程對象
    Thread t = Thread.currentThread();
    //  獲取該線程的threadLocals屬性(ThreadLocalMap對象)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

//  Thread類中的threadLocals定義
ThreadLocal.ThreadLocalMap threadLocals = null;

//  ThreadLocal中getMap方法
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//  ThreadLocal中createMap方法
//  爲線程創建ThreadLocalMap對象並賦值給threadLocals
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

從源碼中看到在set方法裏面就是把值存入ThreadLocalMap類中,這個類是屬於ThreadLocal的內部類,但是在Thread類中也有定義threadLocals變量,get方法的操作對象也是ThreadLocalMap,也就是說關鍵的存儲和獲取實質上在於ThreadLocalMap類。其中是以ThreadLocal類爲key,存入的值爲value,而ThreadLocal又是定義在每個線程的屬性中,這也就實現了“ThreadLocal線程私有化”的功能,每次都是先從當前線程獲取到threadLocals屬性,也就是獲得ThreadLocalMap對象,以ThreadLocal對象作爲key存取對應的值

3.2 探究ThreadLocalMap對象

ThreadLocalMap對象是ThreadLocal類的內部類,其中它就是一個簡單的Map,每個存的值被封裝成Entry進行存儲,下面是Entry的源碼

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

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

Entry是ThreadLocalMap的內部類,仔細觀察其源碼發現,它是繼承了一個ThreadLocal的弱引用。回憶一下Java中的四種引用:強引用、軟引用、弱引用、幻象引用。強引用是new創建出來的對象,只要強引用存在,垃圾收集器永遠不會回收該引用;軟引用(SoftReference)是在內存即將被佔滿時被回收;弱引用(WeakReference)用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次GC發生之前,當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉該類對象;幻象引用又稱虛引用或幽靈引用(Phantom Reference),它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得對象實例,任何時候都可能被回收。更多的解釋和示例讀者可前往我之前寫的這篇文章閱讀:

3.3 ThreadLocal對象的回收

Entry是一個弱引用,是因爲它不會影響到ThreadLocal的GC行爲,如果是強引用的話,在線程運行過程中,我們不再使用ThreadLocal了,將ThreadLocal置爲null,但ThreadLocal在線程的ThreadLocalMap裏還有引用,導致其無法被GC回收。而Entry聲明爲WeakReference,ThreadLocal置爲null後,線程的ThreadLocalMap就不算強引用了,ThreadLocal就可以被GC回收了。

//  ThreadLocal中的remove方法
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

//  ThreadLocalMap中的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;
        }
    }
}

可能存在的內存泄漏問題

ThreadLocalMap的生命週期是與線程一樣的,但是ThreadLocal卻不一定,可能ThreadLocal使用完了就想要被回收,但是此時線程可能不會立即終止,還會繼續運行(比如線程池中線程重複利用),如果ThreadLocal對象只存在弱引用,那麼在下一次垃圾回收的時候必然會被清理掉。

如果ThreadLocal沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來ThreadLocalMap中使用這個ThreadLocal的key也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現key爲null的value

在ThreadLocalMap中,調用 set()、get()、remove()方法的時候,會清理掉key爲null的記錄。在ThreadLocal設置爲null之後,ThreadLocalMap中存在key爲null的值,那麼就可能發生內存泄漏,只有手動調用remove()方法來避免,所以我們在使用完ThreadLocal變量時,儘量用threadLocal.remove()來清除,避免threadLocal=null的操作。remove方法是徹底地回收該對象,而通過threadLocal=null只是釋放掉了ThreadLocal的引用,但是在ThreadLocalMap中卻還存在其Entry,後續還需處理。

四、ThreadLocal應用場景

  • 處理較爲複雜的業務時,使用ThreadLocal代替參數的顯示傳遞。
  • ThreadLocal可以用來做數據庫連接池保存Connection對象,這樣就可以讓線程內多次獲取到的連接是同一個了(Spring中的DataSource就是使用的ThreadLocal)。
  • 管理Session會話,將Session保存在ThreadLocal中,使線程處理多次處理會話時始終是一個Session。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章