該文章屬於《Android Handler機制之》系列文章,若是想了解更多,請點擊 《Android Handler機制之總目錄》java
要想了解Android 的Handle機制,咱們首先要了解ThreadLocal,根據字面意思咱們都能猜出個大概。就是線程本地變量。那麼咱們把變量存儲在本地有什麼好處呢?其中的原理又是什麼呢?下面咱們就一塊兒來討論一下ThreadLocal的使用與原理。數組
該類提供線程局部變量。這些變量不一樣於它們的正常變量,即每個線程訪問自身的局部變量時,都有它本身的,獨立初始化的副本。該變量一般是與線程關聯的私有靜態字段,列如用於ID或事物ID。你們看了介紹後,有可能仍是不瞭解其主要的主要做用,簡單的畫個圖幫助你們理解。bash
從圖上能夠看出,經過ThreadLocal,每一個線程都能獲取本身線程內部的私有變量,有可能你們以爲無圖無真相,「你一我的在那裏神吹,我怎麼知道你說的對仍是不對呢?」,下面咱們經過具體的例子詳細的介紹,來看下面的代碼。ide
class ThreadLocalTest {
//會出現內存泄漏的問題,下文會描述
private static ThreadLocal<String> mThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
mThreadLocal.set("線程main");
new Thread(new A()).start();
new Thread(new B()).start();
System.out.println(mThreadLocal.get());
}
static class A implements Runnable {
@Override
public void run() {
mThreadLocal.set("線程A");
System.out.println(mThreadLocal.get());
}
}
static class B implements Runnable {
@Override
public void run() {
mThreadLocal.set("線程B");
System.out.println(mThreadLocal.get());
}
}
}
複製代碼
在上訴代碼中,咱們在主線程中設置mThreadLocal的值爲"線程main",在線程A中設置爲」線程A「,在線程B中設置爲」線程B",運行程序打印結果以下圖所示:post
main
線程A
線程B
複製代碼
從上面結果能夠看出,雖然是在不一樣的線程中訪問的同一個變量mThreadLocal,可是他們經過ThreadLocl獲取到的值倒是不同的。也就驗證了上面咱們所畫的圖是正確的了,那麼如今,咱們已經知道了ThreadLocal的用法,那麼咱們如今來看看其中的內部原理。ui
爲了幫助你們快速的知曉ThreadLocal原理,這裏我將ThreadLocal的原理用下圖表示出來了:this
在上圖中咱們能夠發現,整個ThreadLocal的使用都涉及到線程中ThreadLocalMap
,雖然咱們在外部調用的是ThreadLocal.set(value)方法,但本質是經過線程中的ThreadLocalMap中的set(key,value)方法
,那麼經過該狀況咱們大體也能猜出get方法也是經過ThreadLocalMap。那麼接下來咱們一塊兒來看看ThreadLocal中set與get方法的具體實現與ThreadLocalMap的具體結構。spa
在使用ThreadLocal時,咱們會調用ThreadLocal的set(T value)方法對線程中的私有變量設置,咱們來查看ThreadLocal的set方法線程
public void set(T value) {
Thread t = Thread.currentThread();//獲取當前線程
ThreadLocalMap map = getMap(t);//拿到線程的LocalMap
if (map != null)
map.set(this, value);//設值 key->當前ThreadLocal對象。value->爲當前賦的值
else
createMap(t, value);//建立新的ThreadLocalMap並設值
}
複製代碼
當調用set(T value) 方法時,方法內部會獲取當前線程中的ThreadLocalMap,獲取後進行判斷,若是不爲空,就調用ThreadLocalMap的set方法(其中key爲當前ThreadLocal對象,value爲當前賦的值)。反之,讓當前線程建立新的ThreadLocalMap並設值,其中getMap()與createMap()方法具體代碼以下:翻譯
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製代碼
簡簡單單的經過ThreadLocalMap的set()方法,咱們已經大體瞭解了。ThreadLocal爲何能操做線程內的私有數據了,ThreadLocal中全部的數據操做都與線程中的ThreadLocalMap有關,同時那咱們接下來看看ThreadLocalMap相關代碼。
ThreadLocalMap是ThreadLocal中的一個靜態內部類,官方的註釋寫的很全面,這裏我大概的翻譯了一下,ThreadLocalMap是爲了維護線程私有值建立的自定義哈希映射。其中線程的私有數據都是很是大且使用壽命長的數據(其實想想,爲何要存儲這些數據呢,第一是爲了把經常使用的數據放入線程中提升了訪問的速度,第二是若是數據是很是大的,避免了該數據頻繁的建立,不只解決了存儲空間的問題,也減小了沒必要要的IO消耗)。
ThreadLocalMap 具體代碼以下:
static class ThreadLocalMap {
//存儲的數據爲Entry,且key爲弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//table初始容量
private static final int INITIAL_CAPACITY = 16;
//table 用於存儲數據
private Entry[] table;
//負載因子,用於數組容量擴容
private int threshold; // Default to 0
//負載因子,默認狀況下爲當前數組長度的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//第一次放入Entry數據時,初始化數組長度,定義擴容閥值,
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//初始化數組長度爲16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);//閥值爲當前數組默認長度的2/3
}
複製代碼
從代碼中能夠看出,雖然官方申明爲ThreadLocalMap是一個哈希表,可是它與咱們傳統認識的HashMap等哈希表內部結構是不同的。ThreadLocalMap內部僅僅維護了Entry[] table,數組。其中Entry實體中對應的key爲弱引用(下文會將爲何會用弱引用),在第一次放入數據時,會初始化數組長度(爲16),定義數組擴容閥值(當前默認數組長度的2/3)。
private void set(ThreadLocal<?> key, Object value) {
//根據哈希值計算位置
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//判斷當前位置是否有數據,若是key值相同,就替換,若是不一樣則找空位放數據。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//獲取下一個位置的數據
ThreadLocal<?> k = e.get();
//判斷key值相同否,若是是直接覆蓋 (第一種狀況)
if (k == key) {
e.value = value;
return;
}
//若是當前Entry對象對應Key值爲null,則清空全部Key爲null的數據(第二種狀況)
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//以上狀況都不知足,直接添加(第三種狀況)
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)//若是當前數組到達閥值,那麼就進行擴容。
rehash();
}
複製代碼
直接經過代碼理解比較困難,這裏直接將set方法分爲了三個步驟,下面咱們咱們就分別對這個三個步驟,分別經過圖與代碼的方式講解。
若是當前數組中,若是當前位置對應的Entry的key值與新添加的Entry的key值相同,直接進行覆蓋操做。具體狀況以下圖所示
若是當前數組中。存在key值相同的狀況,ThreadLocal內部操做是直接覆蓋的。這種狀況就不過多的介紹了。
第二種狀況相對來講比較複雜,這裏先給圖,而後會根據具體代碼來說解。
從圖中咱們能夠看出來。當咱們添加新Entry(key=19,value =200,index = 3)時,數組中已經存在舊Entry(key =null,value = 19),當出現這種狀況是,方法內部會將新Entry的值所有賦值到舊Entry中,同時會將全部數組中key爲null的Entry所有置爲null(圖中大黃色數據)。在源碼中,當新Entry對應位置存在數據,且key爲null的狀況下,會走replaceStaleEntry
方法。具體代碼以下:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//記錄當前要清除的位置
int slotToExpunge = staleSlot;
//往前找,找到第一個過時的Entry(key爲空)
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)//判斷引用是否爲空,若是爲空,擦除的位置爲第一個過時的Entry的位置
slotToExpunge = i;
//日後找,找到最後一個過時的Entry(key爲空),
for (int i = nextIndex(staleSlot, len);//這裏要注意得到位置有可能爲0,
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//在日後找的時候,若是獲取key值相同的。那麼就從新賦值。
if (k == key) {
//賦值到以前傳入的staleSlot對應的位置
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//若是往前找的時候,沒有過時的Entry,那麼就記錄當前的位置(日後找相同key的位置)
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//那麼就清除slotToExpunge位置下全部key爲null的數據
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//若是往前找的時候,沒有過時的Entry,且key =null那麼就記錄當前的位置(日後找key==null位置)
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 把當前key爲null的對應的數據置爲null,並建立新的Entry在該位置上
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//若是日後找,沒有過時的實體,
//且staleSlot以前能找到第一個過時的Entry(key爲空),
//那麼就清除slotToExpunge位置下全部key爲null的數據
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
複製代碼
上面代碼看起來比較繁雜,可是你們仔細梳理就會發現其實該方法,主要對四種狀況進行了判斷,具體狀況以下圖表所示:
咱們已經瞭解了replaceStaleEntry方法內部會清除key==null的數據,而其中具體的方法與expungeStaleEntry()方法與cleanSomeSlots()方法有關,因此接下來咱們來分析這兩個方法。看看其的具體實現。
expungeStaleEntry ()方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 將staleSlot位置下的數據置爲null
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
//日後找。
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {//清除key爲null的數據
e.value = null;
tab[i] = null;
size--;
} else {
//若是key不爲null,可是該key對應的threadLocalHashCode發生變化,
//計算位置,並將元素放入新位置中。
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;//返回最後一個tab[i]) != null的位置
}
複製代碼
expungeStaleEntry()方法主要乾了三件事,第一件,將staleSlot的位置對應的數據置爲null,第二件,刪除並刪除此位置後對應相關聯位置key = null的數據。第三件,若是若是key不爲null,可是該key對應的threadLocalHashCode發生變化,計算變化後的位置,並將元素放入新位置中。
cleanSomeSlots()方法
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;//若是有過時的數據被刪除,就返回true,反之false
}
複製代碼
在瞭解了expungeStaleEntry()方法後,再來理解cleanSomeSlots()方法就很簡單了。其中第一個參數表示開始掃描的位置,第二個參數是掃描的長度。從代碼咱們明顯的看出。就是簡單的遍歷刪除全部位置下key==null的數據。
圖上爲了方便你們,理解清空上下數據的狀況,我並無從新計算位置(但願你們注意!!!)
看到這裏,爲了方便你們避免沒必要要的查閱代碼,我直接將代碼貼出來了。代碼以下。
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
複製代碼
從上述代碼其實,你們很明顯的看出來,就是清除key==null的數據,判斷當前數據的長度是否是到達閥值(默認沒擴容前爲INITIAL_CAPACITY *2/3,其中INITIAL_CAPACITY = 16),若是達到了從新計算數據的位置。關於rehash()方法,具體代碼以下:
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
//清空全部key==null的數據
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);
}
}
//從新計算key!=null的數據。新的數組長度爲以前的兩倍
private void resize() {
//對原數組進行擴容,容量爲以前的兩倍
Entry[] oldTab = table;
int oldLen = oldTab.length;
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++;
}
}
}
//從新計算閥值(負載因子)爲擴容以後的數組長度的2/3
setThreshold(newLen);
size = count;
table = newTab;
}
複製代碼
rehash內部全部涉及到的方法,我都列舉出來了。能夠看出在添加數據的時候,會進行判斷是否擴容操做,若是須要擴容,會清除全部的key==null的數據,(也就是調用expungeStaleEntries()方法,其中expungeStaleEntry()方法已經介紹了,就不過多描述),同時會從新計算數據中的位置。
在瞭解了ThreadLocal的set()方法以後,咱們看看怎麼獲取ThreadLocal中的數據,具體代碼以下:
public T get() {
Thread t = Thread.currentThread();//獲取當前線程
ThreadLocalMap map = getMap(t);//拿到線程中的Map
if (map != null) {
//根據key值(ThreadLocal)對象,獲取存儲的數據
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//若是ThreadLocalMap爲空,建立新的ThreadLocalMap
return setInitialValue();
}
複製代碼
其實ThreadLocal的get方法其實很簡單,就是獲取當前線程中的ThreadLocalMap對象,若是沒有則建立,若是有,則根據當前的 key(當前ThreadLocal對象),獲取相應的數據。其中內部調用了ThreadLocalMap的getEntry()方法區獲取數據,咱們繼續查看getEntry()方法。
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);
}
複製代碼
getEntry()方法內部也很簡單,也只是根據當前key哈希後計算的位置,去找數組中對應位置是否有數據,若是有,直接將數據放回,若是沒有,則調用getEntryAfterMiss()方法,咱們繼續往下看 。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)//若是key相同,直接返回
return e;
if (k == null)//若是key==null,清除當前位置下全部key=null的數據。
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;//沒有數據直接返回null
}
複製代碼
從上述代碼咱們能夠知道,若是從數組中,獲取的key==null的狀況下,get方法內部也會調用expungeStaleEntry()方法,去清除當前位置全部key==null的數據,也就是說如今不論是調用ThreadLocal的set()仍是get()方法,都會去清除key==null的數據。
經過整個ThreadLocal機制的探索,我相信你們確定會有一個疑惑,爲何ThreadLocalMap中採用是的是弱引用做爲Key?
關於該問題,涉及到Java的回收機制。
在Java中判斷一個對象究竟是不是須要回收,都跟引用相關。在Java中引用分爲了4類。
經過該知識點的瞭解後,咱們再來了解爲何ThreadLocal不能使用強引用,若是key使用強引用,那麼當引用ThreadLocal的對象被回收了,但ThreadLocalMap中還持有ThreadLocal的強引用,若是沒有手動刪除,ThreadLocal不會被回收,致使內存泄漏。
當咱們知道了爲何採用弱引用來做爲ThreadLocalMap中的key的知識點後,這個時候又會引伸出另外一個問題不論是調用ThreadLocal的set()仍是get()方法,都會去清除key==null的數據。爲毛咱們要去清除那些key==null的Entry呢?
爲何清除key==null的Entry主要有如下兩個緣由,具體以下所示:
經過以上分析,咱們能夠了解在ThreadLocalMap的設計中其實已經考慮到上述兩種狀況,也加上了一些防禦措施。(在調用ThreadLocal的get(),set(),remove()方法的時候都會清除線程ThreadLocalMap裏全部key爲null的Entry)
雖然ThreadLocal幫咱們考慮了內存泄漏的問題,爲咱們加上了一些防禦措施。可是在實際使用中,咱們仍是須要注意避免如下兩種狀況,下述兩種狀況仍然有可能會致使內存泄漏。
使用static修飾的ThreadLocal,延長了ThreadLocal的生命週期,可能致使的內存泄漏。具體緣由是在Java虛擬機在加載類的過程當中爲靜態變量分配內存。static變量的生命週期取決於類的生命週期,也就是說類被卸載時,靜態變量纔會被銷燬並釋放內存空間。而類的生命週期結束與下面三個條件相關。
其實理解起來也很簡單,就是第一次調用了ThreadLocal設置數據後,就不在調用get()、set()、remove()方法。也就是說如今ThreadLocalMap中就只有一條數據。那麼若是調用ThreadLocal的線程一直不結束的話,即便ThreadLocal已經被置爲null(被GC回收),也一直存在一條強引用鏈:Thread Ref(當前線程引用) -> Thread -> ThreadLocalMap -> Entry -> value,致使數據沒法回收,形成內存泄漏。