同一個threadlocal變量所包含的對象,在不一樣的線程中是不一樣的副本。數組
既然是不一樣的線程擁有不一樣的副本且不容許其餘線程訪問,因此不存在共享變量的問題。安全
解決的是變量在線程間隔離在方法或類之間共享的問題。函數
解決這個問題可能有的兩種方案this
第一種:一個threadlocal對應一個map,map中以thread爲key
spa
這種方法多個線程針對同一個threadlocal1是同一個map,新增線程或減小線程都須要改動map,這個map就變成了多個線程之間的共享變量,須要額外機制好比鎖保證map的線程安全。線程
線程結束時,須要保證它所訪問的全部 ThreadLocal 中對應的映射均刪除,不然可能會引發內存泄漏。-----爲何會致使內存泄漏呢?見下文code
第二種:一個thread對應一個map,map中以threadlocal爲key區分對象
Thread中有一個變量blog
ThreadLocal.ThreadLocalMap threadLocals = null;//每一個線程維護一個map
ThreadLocalMap索引
static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } } private Entry[] table;
ThreadLocalMap維護了一個table數組,存儲Entry類型對象,Entry類型對象以ThreadLocal爲key,任意對象爲值的健值對
ThreadLocal的get set
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);//返回當前線程維護的threadlocals變量 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } 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 getMap(Thread t) { return t.threadLocals; }
Entry key的弱引用以及內存泄漏
爲何說threadlocal會存在內存泄漏:
每一個thread維護的threadlocalmap key是指向threadlocal的弱引用,當沒有任何其餘強引用指向threadlocal的時候,gc會把key回收。
但value是是thread指向的強引用,thread不結束,value不會被回收。
因此當threadlocal不可用但thread還在的這段時間內,會存在所說的內存泄漏。尤爲當使用線程池的時候,線程被複用。
jdk有沒有相應的處理:
再回到threadlocalmap的get set方法
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //獲取當前線程的map if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); //找到當前的threadlocal if (e != null) return (T)e.value;//取值 } return setInitialValue();// map爲null或者map找不到指定key時,初始化基本值,不展開 } //getEntry函數 private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1);//根據key計算索引 Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e);//table中該索引位置對象e爲null 或者 索引位置key不符進入getEntryAfterMiss } //getEntryAfterMiss函數 private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; //遍歷table一直到找到了k=key的位置,返回相應對象e //遍歷過程當中若是遇到了k爲null,即調用expungeStaleEntry清理該entry,即前面所說的內存泄漏,這裏是處理的一個時機 while (e != null) { ThreadLocal k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); //循環遍歷table,ThreadLocal採用的是開放地址法,即有衝突後,把要插入的元素放在要插入的位置後面爲null的地方 e = tab[i]; } return null;//若是是e爲null 返回null } //expungeStaleEntry 函數 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot:key爲null的索引位置的對象.value置爲null,對象也置爲null 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; //遍歷是從staleSlot以後到遇到的第一個e爲null i = nextIndex(i, len)) { ThreadLocal k = e.get(); if (k == null) {//遍歷的過程當中遇到key爲null作和上面一樣的處理 e.value = null; tab[i] = null; size--; } else { //key不爲null的從新hash 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; } //再看set public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } //重點在map.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)]) { //若是根據索引找到的entry不是空的 ThreadLocal k = e.get(); if (k == key) { //key相同,value直接覆蓋 e.value = value; return; } if (k == null) { //遍歷過程當中key爲null,清除 replaceStaleEntry(key, value, i); return; } } //上面沒有處理掉,找到第一個爲null的能用的坑位,new一個entry放入 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
能大體看到,上面的代碼中,threadlocalmap的get set都會作對key爲null的清除工做,從而解決了上面說的內存泄漏問題,只是這種處理依賴對set get的調用
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; } } }
綜上所述,不少地方會看到有這樣的兩條建議:
1 .使用者須要手動調用remove函數,刪除再也不使用的ThreadLocal.
2 .還有儘可能將ThreadLocal設置成private static的,這樣ThreadLocal會盡可能和線程自己一塊兒消亡。