Android Handler機制之ThreadLocal

小積木.jpg

該文章屬於《Android Handler機制之》系列文章,若是想了解更多,請點擊 《Android Handler機制之總目錄》java

前言

要想了解Android 的Handle機制,咱們首先要了解ThreadLocal,根據字面意思咱們都能猜出個大概。就是線程本地變量。那麼咱們把變量存儲在本地有什麼好處呢?其中的原理又是什麼呢?下面咱們就一塊兒來討論一下ThreadLocal的使用與原理。數組

ThreadLocal簡單介紹

該類提供線程局部變量。這些變量不一樣於它們的正常變量,即每個線程訪問自身的局部變量時,都有它本身的,獨立初始化的副本。該變量一般是與線程關聯的私有靜態字段,列如用於ID或事物ID。你們看了介紹後,有可能仍是不瞭解其主要的主要做用,簡單的畫個圖幫助你們理解。bash

ThreadLocal示意圖.png

從圖上能夠看出,經過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原理,這裏我將ThreadLocal的原理用下圖表示出來了:this

threadLocal.png

在上圖中咱們能夠發現,整個ThreadLocal的使用都涉及到線程中ThreadLocalMap,雖然咱們在外部調用的是ThreadLocal.set(value)方法,但本質是經過線程中的ThreadLocalMap中的set(key,value)方法,那麼經過該狀況咱們大體也能猜出get方法也是經過ThreadLocalMap。那麼接下來咱們一塊兒來看看ThreadLocal中set與get方法的具體實現與ThreadLocalMap的具體結構。spa

ThreadLocal的set方法

在使用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 內部結構

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)。

ThreadLocalMap 的set()方法

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方法分爲了三個步驟,下面咱們咱們就分別對這個三個步驟,分別經過圖與代碼的方式講解。

第一種狀況, Key值相同

若是當前數組中,若是當前位置對應的Entry的key值與新添加的Entry的key值相同,直接進行覆蓋操做。具體狀況以下圖所示

key值相同狀況.png

若是當前數組中。存在key值相同的狀況,ThreadLocal內部操做是直接覆蓋的。這種狀況就不過多的介紹了。

第二種狀況,若是當前位置對應Entry的Key值爲null

第二種狀況相對來講比較複雜,這裏先給圖,而後會根據具體代碼來說解。

對應位置Key值爲null.png

從圖中咱們能夠看出來。當咱們添加新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);
        }

複製代碼

上面代碼看起來比較繁雜,可是你們仔細梳理就會發現其實該方法,主要對四種狀況進行了判斷,具體狀況以下圖表所示:

TIM截圖20180731110649.png

咱們已經瞭解了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的數據。

第三種狀況,當前對應位置爲null

沒有數據的狀況.png

圖上爲了方便你們,理解清空上下數據的狀況,我並無從新計算位置(但願你們注意!!!)

看到這裏,爲了方便你們避免沒必要要的查閱代碼,我直接將代碼貼出來了。代碼以下。

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的get()方法

在瞭解了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內存泄漏的問題

經過整個ThreadLocal機制的探索,我相信你們確定會有一個疑惑,爲何ThreadLocalMap中採用是的是弱引用做爲Key?關於該問題,涉及到Java的回收機制。

爲何使用弱引用

在Java中判斷一個對象究竟是不是須要回收,都跟引用相關。在Java中引用分爲了4類。

  • 強引用:只要引用存在,垃圾回收器永遠不會回收Object obj = new Object();而這樣 obj對象對後面new Object的一個強引用,只有當obj這個引用被釋放以後,對象纔會被釋放掉。
  • 軟引用:是用來描述,一些還有但並不是必須的對象,對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。(SoftReference)
  • 弱引用:也是用來描述非必須的對象,可是它的強度要比軟引用更弱一些。被弱引用關聯的對象只能生存到下一次垃圾收集發生以前,當垃圾收集器工做是,不管當前內存是否足夠,都會回收掉被弱引用關聯的對象。(WeakReference)
  • 虛引用:也被稱爲幽靈引用,它是最弱的一種關係。一個對象是否有引用的存在,徹底不會對其生存時間構成影響,也沒法經過一個虛引用來取得一個實例對象。

經過該知識點的瞭解後,咱們再來了解爲何ThreadLocal不能使用強引用,若是key使用強引用,那麼當引用ThreadLocal的對象被回收了,但ThreadLocalMap中還持有ThreadLocal的強引用,若是沒有手動刪除,ThreadLocal不會被回收,致使內存泄漏。

弱引用帶來的問題

當咱們知道了爲何採用弱引用來做爲ThreadLocalMap中的key的知識點後,這個時候又會引伸出另外一個問題不論是調用ThreadLocal的set()仍是get()方法,都會去清除key==null的數據。爲毛咱們要去清除那些key==null的Entry呢?

爲何清除key==null的Entry主要有如下兩個緣由,具體以下所示:

  • 從上面咱們已經知道了,ThreadLocalMap使用ThreadLocal的弱引用做爲key,也就是說,若是一個ThreadLocal沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal勢必會被回收。這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,
  • 若是當前線程遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:Thread Ref(當前線程引用) -> Thread -> ThreadLocalMap -> Entry -> value,那麼將會致使這些Entry永遠沒法回收,形成內存泄漏。

經過以上分析,咱們能夠了解在ThreadLocalMap的設計中其實已經考慮到上述兩種狀況,也加上了一些防禦措施。(在調用ThreadLocal的get(),set(),remove()方法的時候都會清除線程ThreadLocalMap裏全部key爲null的Entry)

ThreadLocal使用注意事項

雖然ThreadLocal幫咱們考慮了內存泄漏的問題,爲咱們加上了一些防禦措施。可是在實際使用中,咱們仍是須要注意避免如下兩種狀況,下述兩種狀況仍然有可能會致使內存泄漏。

避免使用static的ThreadLocal

使用static修飾的ThreadLocal,延長了ThreadLocal的生命週期,可能致使的內存泄漏。具體緣由是在Java虛擬機在加載類的過程當中爲靜態變量分配內存。static變量的生命週期取決於類的生命週期,也就是說類被卸載時,靜態變量纔會被銷燬並釋放內存空間。而類的生命週期結束與下面三個條件相關。

  1. 該類全部的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  2. 加載該類的ClassLoader已經被回收。
  3. 該類對應的java.lang.Class對象沒有任何地方被引用,沒有在任何地方經過反射訪問該類的方法。

分配使用了ThreadLocal又再也不調用get(),set(),remove()方法

其實理解起來也很簡單,就是第一次調用了ThreadLocal設置數據後,就不在調用get()、set()、remove()方法。也就是說如今ThreadLocalMap中就只有一條數據。那麼若是調用ThreadLocal的線程一直不結束的話,即便ThreadLocal已經被置爲null(被GC回收),也一直存在一條強引用鏈:Thread Ref(當前線程引用) -> Thread -> ThreadLocalMap -> Entry -> value,致使數據沒法回收,形成內存泄漏。

總結

  • ThreadLocal本質是操做線程中ThreadLocalMap來實現本地線程變量的存儲的
  • ThreadLocalMap是採用數組的方式來存儲數據,其中key(弱引用)指向當前ThreadLocal對象,value爲設的值
  • ThreadLocal爲內存泄漏採起了處理措施,在調用ThreadLocal的get(),set(),remove()方法的時候都會清除線程ThreadLocalMap裏全部key爲null的Entry
  • 在使用ThreadLocal的時候,咱們仍然須要注意,避免使用static的ThreadLocal,分配使用了ThreadLocal後,必定要根據當前線程的生命週期來判斷是否須要手動的去清理ThreadLocalMap中清key==null的Entry。
相關文章
相關標籤/搜索