在一般的網站開發中,基於Java的Web 框架都使用了ThreadLocal來存儲一些全局的參數,在攔截器\Filter中設置變量,讓變量可以在任意地方被獲取。
一早就了解到里面有用到WeakReference(弱引用),但對弱引用僅限於一種懵懂的概念,並且認為只要GC,弱引用的對象就被回收掉了,實際情況呢?
Thread對象有一個變量名為 threadLocals 的 ThreadLocalMap對象,這個類和HashMap類似,里面定義了一個Entry數組,不過這個Entry對象弱引用了ThreadLocal
/** * 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); //調用父類構造函數,設置Reference的referent屬性 value = v; } } /** * Set the value associated with key. * * @param key the thread local object * @param value the value to be 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(); 獲取Reference的referent屬性 if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); //清理掉陳舊Entry,這種Entry是由於ThreadLocal變量對象被回收后k==null造成的 return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
注意以下方法
if (k == null) { replaceStaleEntry(key, value, i); //清理掉陳舊Entry,這種Entry是由於ThreadLocal變量對象被回收后k==null造成的 return; }
這個地方其實也解釋了通常所說的ThreadLocal變量可能導致內存溢出的問題:
Entry弱引用了ThreadLocal,因此Entry是否存活,不會影響ThreadLocal的生命周期,ThreadLocal在沒有其他對象引用后被回收,但是它對應的Entry中的value實際還在被Entry引用,而這不是一個弱引用,如果不清掉掉Entry,value就被一直強引用,無法釋放,那么就可能會內存溢出
如果一直沒有ThreadLocal變量訪問,並且線程一直存活,就不會清理陳舊Entry,value永遠無法釋放,這也是某種意義上的內存泄露
一直以來,我把WeakReference理解成:弱引用的對象一定會在下次GC時回收掉,按此推斷,ThreadLocal變量是不太安全的,因為用着用着就可能被GC掉了。
但是如果ThreadLocal有這么嚴重的問題,誰會去用呢!
實際上對象B弱引用了A,如果A除了B以外,沒有其他引用(強、軟引用)時,才會把A GC調,這也是為什么ThreadLocal實際上是一個安全的操作。
Thread.threadLocals 是一個Map,每個Entry都弱引用了ThreadLocal對象
因此Thread.threadLocals對每個ThreadLocal對象都是弱引用關系
調用ThreadLocal.get()方法
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); 可以看到get實際上是獲取當前線程的ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } /** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
可以理解為一個ThreadLocal實例,在多個線程都存在副本,並且在不同線程設置的值,都不會影響到其他線程,因為ThreadLocal實例是存儲在當前Thread對象上的
也就是說,如果Thread死掉后,它和ThreadLocal對象也就沒有任何關系了,這對我們的程序沒有任何影響,因為我們也沒有機會再取它在Thread中的副本了;
而ThreadLocal變量被設置為null后,雖然Thread中的Entry弱引用了ThreadLocal,即使Thread對象還在,當ThreadLocal對象沒有被其他對象引用后,它就可以被GC調了,Thread對象產生了弱引用,不會影響到ThreadLocal變量的回收
其他知識點:
需要注意的是,當一個線程執行Thread.start()后,如果start()方法已經執行完畢了,雖然Thread對象還在,其實它的生命周期已經結束了,對應的ThreadLocal變量可以被回收
如果線程再次調用Thread.start()方法,會拋出異常,因此不用關注線程再次被啟動,因為它已經無法再被啟動了(PS:線程復用技術並非是重新start,而是在線程內執行多個任務)
以下情況ThreadLocal變量是永遠不會回收的
public class FlagContext { /** * ThreadLocal變量 */ private final static ThreadLocal<Boolean> tbFlag = new ThreadLocal<Boolean>(); }
FlagContext的靜態變量引用了ThreadLocal實例tbFlag,tbFlag是個final 聲明的變量,永遠不會為空,因此,這種情況下,tbFlag對應的值,只會在線程生命周期結束,或調用tbFlag.remove()才會被回收!原因呢:
ThreadLocal永遠被FlagContext強引用了,而Thread和ThreadLocal不管是否是弱引用,即使Thread中的Entry被回收了,也不會被GC掉!