對ThreadLocal感興趣是從1個問題開始的:ThreadLocal在何種情況下會產生內存泄漏?對這個問題的思考不能不去了解ThreadLocal本身的實現和1些細節問題等。接下去順次介紹ThreadLocal的功能,實現細節,使用處景和1些使用建議。
ThreadLocal不是用來解決對象同享訪問問題的,而主要提供了線程保持對象的方法和避免參數傳遞的方便的對象訪問方式。1般情況下,通過ThreadLocal.set()到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的。各個線程中訪問的是不同的對象。
ThreadLocal使用處合主要解決多線程中數據因并發產生不1致的問題。ThreadLocal為每一個線程的中并發訪問的數據提供1個副本,通過訪問副本來運行業務,這樣的結果是耗費了內存,但大大減少了線程同步所帶來的線程消耗,也介紹了線程并發控制的復雜度。
另外,說ThreadLocal使得各線程能夠保持各自獨立的1個對象,其實不是通過ThreadLocal.set()來實現的,而是通過每一個線程中的new對象的操作來創建的對象,每一個線程創建1個,不是甚么對象的拷貝或副本。通過ThreadLocal.set()將這個新創建的對象的援用保存到各線程的自己的1個map(Thread類中的ThreadLocal.ThreadLocalMap的變量)中,每一個線程都有這樣1個map,履行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作為map的key來使用的。
【代碼1】
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
很多人會有這樣的無解:感覺這個ThreadLocal對象建立了1個類似于全局的map,然后每一個線程作為map的key來存取對應的線程本地的value。實際上是ThreadLocal類中有1個ThreadLocalMap靜態內部類,可以簡單的理解為1個map,這個map為每一個線程復制1個變量的“拷貝”存儲其中。下面是ThreadLocalMap的部份源碼:
【代碼2】
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
//部份省略
}
ThreadLocal類中1共有4個方法:
就以get()方法為例
【代碼3】
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
get()方法的源碼如上所示,可以看到map中真實的key是線程ThreadLocal實例本身(ThreadLocalMap.Entry e = map.getEntry(this);中的this)。可以看1下getEntry(ThreadLocal key)的源碼.
【代碼4】
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);
}
那末map中的value是甚么呢?我們繼續來看源碼:
【代碼5】
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;
}
protected T initialValue() {
return null;
}
代碼5中只能夠視察到通過[protected T initialValue()]方法設置了1個初始值,固然也能夠通過set方法來賦值,繼續看源碼:
【代碼6】
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal設置值有兩種方案:1. Override其initialValue方法;2. 通過set設置。
關于重寫initialValue方法可以參考下面這個例子簡便的實現:
【代碼7】
private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>(){
@Override
protected Long initialValue()
{
return System.currentTimeMillis();
}
};
通過代碼1和代碼2的片斷可以看出,在Thread類中保有ThreadLocal.ThreadLocalMap的援用,即在1個Java線程棧中指向了堆內存中的1個ThreadLocal.ThreadLocalMap的對象,此對象中保存了若干個Entry,每一個Entry的key(ThreadLocal實例)是弱援用,value是強援用(這點類似于WeakHashMap)。
用到弱援用的只是key,每一個key都弱援用指向threadLocal,當把threadLocal實例置為null以后,沒有任何強援用指向threadLocal實例,所以threadLocal將會被gc回收,但是value卻不能被回收,由于其還存在于ThreadLocal.ThreadLocalMap的對象的Entry當中。只有當前Thread結束以后,所有與當前線程有關的資源才會被GC回收。所以,如果在線程池中使用ThreadLocal,由于線程會復用,而又沒有顯示的調用remove的話的確是會有可能產生內存泄漏的問題。
其實在ThreadLocal.ThreadLocalMap的get或set方法中會探測其中的key是不是被回收(調用expungeStaleEntry方法),然后將其value設置為null,這個功能幾近和WeakHashMap中的expungeStaleEntries()方法1樣。因此value在key被gc后可能還會存活1段時間,但終究也會被回收,但是若不再調用get或set方法時,那末這個value就在線程存活期間沒法被釋放。
【代碼8】
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
其實ThreadLocal本身可以看成是沒有內存泄漏問題的,通過顯示的調用remove方法便可。
ThreadLocal的利用場景,最合適的是按線程多實例(每一個線程對應1個實例)的對象的訪問,并且這個對象很多地方都要用到。
對多線程資源同享的問題,同步機制采取了“以時間換空間”的方式,比如定義1個static變量,同步訪問,而ThreadLocal采取了“以空間換時間”的方式。前者僅提供1份變量,讓不同的線程排隊訪問,而后者為每個線程都提供了1份變量,因此可以同時訪問而互不影響。
在多線程的開發中,常常會斟酌到的策略是對1些需要公然訪問的屬性通過設置同步的方式來訪問。這樣每次能保證只有1個線程訪問它,不會有沖突。但是這樣做的結果會使得性能和對高并發的支持不夠。在某些情況下,如果我們不1定非要對1個變量同享不可,而是給每一個線程1個這樣的資源副本,讓他們可以獨立都各自跑各自的,這樣不是可以大幅度的提高并行度和性能了嗎?
還有的情況是有的數據本身不是線程安全的,或說它只能被1個線程使用,不能被其它線程同時使用。如果等1個線程使用完了再給另外一個線程使用就根本不現實。這樣的情況下,我們也能夠斟酌用ThreadLocal。
ThreadLocal建議:
1. ThreadLocal類變量由于本身定位為要被多個線程來訪問,它通常被定義為static變量。
2. 能夠通過值傳遞的參數,不要通過ThreadLocal存儲,以避免造成ThreadLocal的濫用。
3. 在線程池的情況下,在ThreadLocal業務周期處理完成時,最好顯示的調用remove()方法,清空“線程局部變量”中的值。
4. 在正常情況下使用ThreadLocal不會造成OOM, 弱援用的知識ThreadLocal,保存值仍然是強援用,如果ThreadLocal仍然被其他對象利用,線程局部變量將沒法回收。
InheritableThreadLocal是ThreadLocal的子類,代碼量很少,可以看1下:
【代碼9】
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
這里主要的還是1個childValue這個方法。
在代碼7中示范了ThreadLocal的方法,而使用類InheritableThreadLocal可以在子線程中獲得父線程繼承下來的值。可以采取重寫childValue(Object parentValue)方法來更改繼承的值。
查看案例:
【代碼10】
public class InheriableThreadLocal
{
public static final InheritableThreadLocal<?> itl = new InheritableThreadLocal<Object>(){
@Override protected Object initialValue()
{
return new Date().getTime();
}
@Override protected Object childValue(Object parentValue)
{
return parentValue+" which plus in subThread.";
}
};
public static void main(String[] args)
{
System.out.println("Main: get value = "+itl.get());
Thread a = new Thread(new Runnable(){
@Override public void run()
{
System.out.println(Thread.currentThread().getName()+": get value = "+itl.get());
}
});
a.start();
}
}
運行結果:
Main: get value = 1467100984858
Thread-0: get value = 1467100984858 which plus in subThread.
如果去掉@Override protected Object childValue(Object parentValue)方法運行結果:
Main: get value = 1461585396073
Thread-0: get value = 1461585396073
參考資料
1. Java多線程知識小抄集(1)
2. 深入JDK源碼之ThreadLocal類
3. Java集合框架:WeakHashMap