前言html
栗子java
類圖編程
ThreadLocal源碼分析api
ThreadLocalMap 源碼分析數組
ThreadLocal 可能會致使內存泄漏瀏覽器
ThreadLocal 顧名思義就是在每一個線程內部都會存儲只有當前線程才能訪問的變量的一個副本,而後當前線程修改了該副本的值後而不會影響其餘線程的值,各個變量之間相互不影響。安全
當咱們須要共享一個變量,而該變量又不是線程安全的時候,可使用 ThreadLocal 來複制該變量的一個副本;又好比,瀏覽器用戶的登陸信息須要從當前請求request中獲取,若是須要在不少地方會用到該用戶的登陸信息, 一個解決辦法是向這些全部用到的地方傳遞request參數,另一個辦法就是利用ThreadLocal, 獲取登陸信息後把它放到當前線程中的ThradLocal變量中,任何須要的時候從當前線程中取就能夠了。oracle
首先看一個不使用 ThreadLocal 的簡單不成熟栗子,每一個線程都要修改共享變量 i 的值:函數式編程
private int i = 0; private void createThread() throws InterruptedException { Thread thread = new Thread(() -> { i = 0; System.out.println(Thread.currentThread().getName() + " : " + i); i+=10; System.out.println(Thread.currentThread().getName() + " : " + i); }); thread.start(); thread.join(); } public static void main(String[] args) throws InterruptedException { Main m = new Main(); for (int j = 0; j < 5; j++) { m.createThread(); } } 輸出: Thread-0 : 0 Thread-0 : 10 Thread-1 : 0 Thread-1 : 10 Thread-2 : 0 Thread-2 : 10 Thread-3 : 0 Thread-3 : 10 Thread-4 : 0 Thread-4 : 10
在每一個線程修改該共享變量的值以前,都須要重置該變量的值,以後纔會進行修改,這樣結果纔會符合咱們的預期。函數
接下來看下使用 ThreadLocal 是來實現的:
private int i = 0; // 爲每一個線程建立變量 i 的副本 private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> i); private void createThread2() throws InterruptedException { Thread thread = new Thread(() -> { System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get()); threadLocal.set(threadLocal.get() + 10); System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get()); }); thread.start(); thread.join(); } public static void main(String[] args) throws InterruptedException { Main m = new Main(); for (int j = 0; j < 5; j++) { m.createThread2(); } } 輸出: Thread-0 : 0 Thread-0 : 10 Thread-1 : 0 Thread-1 : 10 Thread-2 : 0 Thread-2 : 10 Thread-3 : 0 Thread-3 : 10 Thread-4 : 0 Thread-4 : 10
能夠看到,使用 ThreadLocal 一樣實現上述的效果,可是不須要再每一個線程執行以前重置該共享變量了。
注:使用 join() 方法爲了讓線程順序執行,線程1執行完了線程2再執行
接下來看下 ThreadLocal 的一個實現
從該類圖中,能夠看到,ThreadLocal 並無實現任何的類,也沒有實現任何的接口,它只有兩個內部類,ThreadLocalMap 和 SuppliedThreadLocal,ThreadLocalMap 類中還有一個 Entry 內部類,能夠看到,類結構是很簡單的。SuppliedThreadLocal 只是爲了實現 Java 8 的函數式編程(Lambda表達式),能夠忽略。關於 Java 8 的 Lambda 能夠參考 Lambda表達式 和 Java 8 中的流--Stream
T |
get() 返回當前線程本地變量的值 |
protected T |
initialValue() 初始化當前線程本地變量的值,默認爲null,通常須要重寫該方法 |
void |
remove() 刪除再也不使用的 ThreadLocal |
void |
set(T value) 設置當前線程本地變量的值 |
static <S> ThreadLocal<S> |
withInitial(Supplier<? extends S> supplier) 使用Lambda表達式設置初始值,和 initialValue() 做用是同樣的 |
ThreadLocal 的方法使用都比較簡單,接下來就看看它們是怎麼實現的,
public class ThreadLocal<T> { // 哈希值 private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } //防止哈希衝突 private static final int HASH_INCREMENT = 0x61c88647; // 當前線程的本地變量的初始值,默認爲null,通常須要重寫該方法 protected T initialValue() { return null; } // Lambda 方式設置初始值 public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); } // 構造方法 public ThreadLocal() { } // 獲取 ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // 根據線程,和變量值建立 ThreadLocalMap // 每一個線程都在本身的 ThreadLocalMap void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
上述是 ThreadLocal 的一些輔助的方法,主要方法 set , get 方法主要是在 ThreadLocalMap 中實現,因此須要在下面結合 ThreadLocalMap 中來講。
首先,ThreadLocalMap 是一個自定義的哈希映射,僅僅是用來維護線程本地變量的值,ThreadLocalMap 使用 WeakReferences 做爲鍵,爲了可以及時的GC,關於 WeakReferences ,能夠參考 java虛擬機之初探。
static class ThreadLocalMap { // 內部類,有兩個屬性:ThreadLocal 和 Object // ThreadLocal:做爲key,當key==ull(即entry.get()== null)表示再也不引用該鍵,所以能夠從表中刪除 // Object:本地變量的值 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // Entry數組的初始容量,爲16,必須爲2的冪 private static final int INITIAL_CAPACITY = 16; // Entry數組,可重置大小,數組的長度必須爲2的冪 private Entry[] table; // Entry數組中元素的個數 private int size = 0; //Entry擴容的閾值,默認爲0 private int threshold; //設置閾值,爲 len 的三分之二 private void setThreshold(int len) { threshold = len * 2 / 3; } // Entry數組的下一個索引 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } // Entry數組的上一個索引 private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } ....方法......... }
從上述定義的屬性和類能夠看到,ThreadLocalMap 主要使用數組來實現的,數組的每一項是一個 Entry 對象,Entry 對象中會持有當前線程的引用和當前線程所綁定的變量值。結構以下所示:
接下來看下 ThreadLocalMap 方法的實現,在該部分中,須要結合 ThreadLocal 的方法一塊兒來看,
// 返回當前線程所綁定的本地變量值,若是當前線程爲null,則返回setInitialValue()方法中的值 public T get() { // 獲取當前線程 Thread t = Thread.currentThread(); // 獲取ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 在 ThreadLocalMap 中獲取當前線程對應的Entry,Entry 中存儲了當前線程所綁定的本地變量的值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { // 獲取當前線程所綁定的本地變量的值,並返回 T result = (T)e.value; return result; } } // 若是當前線程沒有的 ThreadLocalMap 中,則返回 setInitialValue 中的值 return setInitialValue(); }
get() 方法不要有如下幾步:
1.獲取當前線程
2.獲取線程內的 ThreadLocalMap,若是map已經存在,則以當前的ThreadLocal爲鍵,獲取Entry對象,並從從Entry中取出值
3.若是 map 不存在,則調用setInitialValue方法執行初始化
如今,來看下若是從 ThreadLocalMap中獲取當前線程所對應的 Entry 對象:
private Entry getEntry(ThreadLocal<?> key) { // 獲取對應線程的hashcode // 計算 Entry數組的索引 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 若是該索引處的Entry對象恰好等於key,則直接返回 if (e != null && e.get() == key) return e; else // 若是上述條件不知足,則進入 getEntryAfterMiss 方法 return getEntryAfterMiss(key, i, e); }
該方法主要是,當在當前的索引中找不到對應的 Entry 對象時執行,在該方法內部,主要是在 Entry 數組中循環查找對應key,若是key爲空,則進行清理操做
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { // 當前 Entry數組 Entry[] tab = table; int len = tab.length; // 若是當前Entry數組 i 對應的位置的Entry對象不爲空 while (e != null) { ThreadLocal<?> k = e.get(); // 若是key等於Entry數組 i 對應的位置的Entry對象,則直接返回 if (k == key) return e; if (k == null) // 若是 Entry 數組 i 對應的位置的 Entry 對象爲空,則刪除該 Entry 對象,resize Entry數組 expungeStaleEntry(i); else // 不然,獲取 Entry 數組的下一個索引位置,繼續查找 i = nextIndex(i, len); e = tab[i]; } return null; }
當在 Entry 數組中對應的位置不存在任何引用的時候,進行 Entry 數組的清理操做,resize Entry 數組:
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 把當前索引對應位置的對象設置爲null tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Entry數組大小減1 // Rehash 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; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
在執行完上述方法後,get() 方法就會獲得一個 Entry 對象,以後返回該對象的value,該value就是當前線程所綁定的本地變量的值。
在上面所說的 get() 方法中,若是 ThreadLocalMap 不存在,則執行 setInitialValue 進行初始化,下面看下setInitialValue:
private T setInitialValue() { // 調用 initialValue 方法,該方法默認返回null,通常須要重寫該方法 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // 若是 ThreadLocalMap 已存在,則設置初值爲 initialValue 方法的返回值 if (map != null) map.set(this, value); else // 若是 ThreadLocalMap 不存在,則建立 createMap(t, value); return value; }
ThreadLocalMap 的 set 方法,主要用來設置其對應的值:
private void set(ThreadLocal<?> key, Object value) { // 當前的Entry數組 Entry[] tab = table; int len = tab.length; // 數組索引 int i = key.threadLocalHashCode & (len-1); // 遍歷 Entry 數組 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 若是在 Entry 找到,則設置value if (k == key) { e.value = value; return; } // 若是當前的ThreadLocal爲空,則調用replaceStaleEntry來更換這個key爲空的Entry if (k == null) { replaceStaleEntry(key, value, i); return; } } // 若是在 Entry 數組中沒有找到對應的key ,則建立,插入到數組中 tab[i] = new Entry(key, value); int sz = ++size; // 清理 Entry 數組中爲null的項,且若是數組大小大於等於咱們設置的閾值,則rehash數組 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
cleanSomeSlots 方法裏面仍是會調用上面所說的 expungeStaleEntry 方法進行清理 Entry數組爲null的項。
若是當Entry數組的大小大於等於設置的閾值的話,Entry數組就須要進行擴容操做:
private void rehash() { // 清空Entry數組 expungeStaleEntries(); // 若是 數組大小大於等於 閾值的 3/4,則擴容 if (size >= threshold - threshold / 4) // 擴容 resize(); }
private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) expungeStaleEntry(j); } }
把 Entry數組的容量擴大爲原來的 2 倍:
private void resize() { // 舊的數組 Entry[] oldTab = table; // 舊數組的長度 int oldLen = oldTab.length; // 新的數組的長度爲舊的的2倍 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; // 複製數據 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { // 從新計算數組的索引值 int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
ThreadLocal 的 set 方法用來設置當前線程所綁定的變量的值,它的實現和setInitialValue差很少,:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // 若是 ThreadLocalMap 存在,則設置值 if (map != null) map.set(this, value); else // 若是 ThreadLocalMap 不存在則建立 createMap(t, value); }
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) // 調用 ThreadLocalMap 的 remove 方法 m.remove(this); }
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; } } }
以上就是 ThreadLocal 的一個實現過程。
從上面的代碼中能夠看到,ThreadLocalMap 使用 ThreadLocal 的弱引用做爲key,若是一個 ThreadLocal 沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal 就會被回收,這樣一來,ThreadLocalMap 中就會出現 key 爲 null 的 Entry,就沒有辦法訪問這些key爲null的Entry的value,若是當前線程再遲遲不結束的話,這些key爲null的Entry的value永遠沒法回收,形成內存泄漏。在 ThreadLocal 中 的 get, set 和remove 方法中,都對 Entry 的key進行的null的判斷,若是爲null,則會 expungeStaleEntry 進行清理操做;
因此,在線程中使用完 ThreadLocal 變量後,要記得及時remove掉。