咱們知道ThreadLocal用於維護多個線程線程獨立的變量副本,這些變量只在線程內共享,可跨方法、類等,以下是一個維護多個線程Integer變量的ThreadLocal:數組
ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();
每一個使用threadLocalNum
的線程,能夠經過形如threadLocalNum.set(1)
的方式建立了一個獨立使用的Integer
變量副本,那麼它是怎麼實現的呢?咱們今天就來簡單的分析一下。安全
先看下ThreadLocal的set方法是如何實現的,源碼以下:this
public void set(T value) { Thread t = Thread.currentThread(); //獲取當前線程 ThreadLocalMap map = getMap(t); //獲取當前線程的ThreadLocalMap if (map != null) map.set(this, value); //當前線程的ThreadLocalMap不爲空則直接設值 else createMap(t, value); //當前線程的ThreadLocalMap爲空則建立一個來設置值 }
是的,你沒有看錯,是獲取當前線程中的ThreadLocalMap
來設置的值,咱們來看一下getMap(t)
是如何實現的:線程
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
而後咱們看到Thread中包含了一個ThreadLocalMap類型的屬性:設計
ThreadLocal.ThreadLocalMap threadLocals = null;
到這裏咱們能夠得出一個結論:各個線程持有了一個ThreadLocalMap的屬性,經過ThreadLocal設置變量時,直接設置到了對應線程的的ThreadLocalMap屬性中。code
那麼不一樣的線程中經過ThreadLocal設置的值是如何關聯定義的ThreadLocal變量和Thread中的ThreadLocalMap的呢?咱們接着分析。對象
前面寫到當前線程的ThreadLocalMap爲空則建立一個ThreadLocalMap來設值,咱們來看下createMap(t, value)
的具體實現:blog
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } /////////////////// //ThreadLocalMap構造器定義以下 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } private static final int INITIAL_CAPACITY = 16;
線程中threadLocals是一個ThreadLocalMap變量,其默認值是null,該線程在首次使用threadLocal對象調用set的時候經過createMap(Thread t, T firstValue)
實例化。繼承
先來看一下ThreadLocalMap,它是在ThreadLocal中定義的一個靜態內部類,其內屬性以下:
/** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; /** * The number of entries in the table. */ private int size = 0; /** * The next size value at which to resize. */ private int threshold; // Default to 0
其中屬性private Entry[] table
,用於存儲經過threadLocal set 進來的變量,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
繼承了WeakReference<ThreadLocal<?>>
,ThreadLocal在構造器中被指定爲弱引用super(k)
(後面會單獨討論爲什麼這裏使用弱引用)。
至此,咱們能夠知道ThreadLocal和Thead的內存結構以下:
網上看到不少文章都在講ThreadLocal的內存泄露問題,因此也在這裏簡單說一下本身的理解。
從上面的結構能夠看出ThreadLocal涉及到的要回收的對象包括:
下面先簡述java的引用,而後分別討論ThreadLocal自己的回收和threadLcoalMap的回收
ThreadLocal實例的引用主要包括兩種:
強引用還在的狀況下ThreadLocal必定不會被回收;無強引用後,因爲各個Thread中Entry的key是弱引用,會在下次GC後變爲null。ThreadLocal實例何時被回收徹底取決於強引用什麼時候被幹掉,那麼何時強引用會被銷燬呢?最簡單的就是 threadLocal=null
強引用被賦值爲null;其它也但是threadLocal是一個局部變量,在方法退出後引用被銷燬,等等。
這裏來回答一下前面提到的爲何ThreadLocalMap中將key設計爲弱引用,咱們假設若是ThreadLocalMap中是強引用會出現什麼狀況?定義ThreadLocal時定義的強引用被置爲null的時候,若是還有其它使用了該ThreadLocal的線程沒有完成,還須要好久會執行完成,那麼這個線程將一直持有該ThreadLocal實例的引用,直到線程完成,期間ThreadLocal實例都不能被回收,最重要的是若是不瞭解ThreadLocal內部實現,你可能都不知道還有其餘線程引用了threadLocal實例。
線程結束時清除ThreadLocalMap的代碼Thread.exit()
以下:
/** * This method is called by the system to give a Thread * a chance to clean up before it actually exits. */ private void exit() { if (group != null) { group.threadTerminated(this); group = null; } /* Aggressively null out all reference fields: see bug 4006245 */ target = null; /* Speed the release of some of these resources */ threadLocals = null; inheritableThreadLocals = null; inheritedAccessControlContext = null; blocker = null; uncaughtExceptionHandler = null; }
單從引用的角度來看,各線程中的threadLocalMap,其中包括各個Entry的key 和 value,線程(也就是Thread實例)自己一直持有threadLocalMap的強引用,只有在線程結束的時候纔會被回收,可是ThreadLocal在實現的時候提供了一些方法:set/get/remove,能夠在執行它們的時候回收其它已經失效(key=null)的entry實例。
這裏就以set爲例看看ThreadLocal是如何回收entry的,ThreadLocal set方法實現以下:
//ThreadLocal public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); // 本次要分析的方法 else createMap(t, value); //這裏前面已經分析了 } //ThreadLocalMap 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); //獲取當前threadLocal實例的hashcode,同時也是table的下標 //這裏for循環找key,是由於hash衝突會使hashcode指向的下標不是真實的存儲位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //找到了設置爲新值 if (k == key) { e.value = value; return; } //entry不爲null,key爲null //說明原來被賦值過,可是原threadLocal已經被回收 if (k == null) { replaceStaleEntry(key, value, i); return; } } //若是下標對應的entry爲null, 則新建一個entry tab[i] = new Entry(key, value); int sz = ++size; //清理threadlocal中其它被回收了的entry(也就是key=null的entry) if (!cleanSomeSlots(i, sz) && sz >= threshold) //rehash rehash(); }
看一下cleanSomeSlots的實現:
//ThreadLocalMap private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { //獲取下一個entry的下標 i = nextIndex(i, len); Entry e = tab[i]; //entry不爲null,key爲null //說明原來被賦值過,可是原threadLocal已經被回收 if (e != null && e.get() == null) { n = len; removed = true; // 刪除已經無效的entry i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 回收無效entry 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(); //entry不爲null,key爲null,應該回收 if (k == null) { e.value = null; tab[i] = null; size--; } else { //rehash的實現 //計算當前entry的k的hashcode,看是下標是否應該爲i //若是不爲i說明,是以前hash衝突放到這兒的,如今須要reash int h = k.threadLocalHashCode & (len - 1); //h!=i 說明hash衝突了, entry不該該放在下標爲i的位置 if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. //找正確的位置h,可是仍是有可能衝突因此要循環 while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
從上面的分析咱們能夠看到把ThreadLocalMap中的key設計爲weakReference,也使set方法能夠經過key==null && entry != null
判斷entry是否失效。
總結一下ThreadLocal set方法的實現:
ThreadLocal經過巧妙的設計最大程度上減小了內存泄露的可能,可是並無徹底消除。
當咱們使用完ThreadLocal後沒有調用set/get/remove方法,那麼可能會致使失效內存不能及時被回收,致使內存泄露,尤爲是在value佔用內存較大的狀況。
因此最佳實踐是,在明確ThreadLocal再也不使用時,手動調用remove方法及時清空。
cleanSomeSlots
方法回收鍵爲 null 的 Entry 對象的值(即失效實例)從而防止內存泄漏(其它的remove,get相似)