Android Handler機(jī)制之ThreadLocal

小積木.jpg

該文章屬于Android Handler系列文章,如果想了解更多,請點(diǎn)擊
《Android Handler機(jī)制之總目錄》

前言

要想了解Android 的Handle機(jī)制,我們首先要了解ThreadLocal,根據(jù)字面意思我們都能猜出個(gè)大概。就是線程本地變量。那么我們把變量存儲(chǔ)在本地有什么好處呢?其中的原理又是什么呢?下面我們就一起來討論一下ThreadLocal的使用與原理。

ThreadLocal簡單介紹

該類提供線程局部變量。這些變量不同于它們的正常變量,即每一個(gè)線程訪問自身的局部變量時(shí),都有它自己的,獨(dú)立初始化的副本。該變量通常是與線程關(guān)聯(lián)的私有靜態(tài)字段,列如用于ID或事物ID。大家看了介紹后,有可能還是不了解其主要的主要作用,簡單的畫個(gè)圖幫助大家理解。

ThreadLocal示意圖.png

從圖上可以看出,通過ThreadLocal,每個(gè)線程都能獲取自己線程內(nèi)部的私有變量,有可能大家覺得無圖無真相,“你一個(gè)人在那里神吹,我怎么知道你說的對還是不對呢?”,下面我們通過具體的例子詳細(xì)的介紹,來看下面的代碼。

class ThreadLocalTest {
    //會(huì)出現(xiàn)內(nèi)存泄漏的問題,下文會(huì)描述
    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());
        }
    }
}

在上訴代碼中,我們在主線程中設(shè)置mThreadLocal的值為"線程main",在線程A中設(shè)置為”線程A“,在線程B中設(shè)置為”線程B",運(yùn)行程序打印結(jié)果如下圖所示:

main
線程A
線程B

從上面結(jié)果可以看出,雖然是在不同的線程中訪問的同一個(gè)變量mThreadLocal,但是他們通過ThreadLocl獲取到的值卻是不一樣的。也就驗(yàn)證了上面我們所畫的圖是正確的了,那么現(xiàn)在,我們已經(jīng)知道了ThreadLocal的用法,那么我們現(xiàn)在來看看其中的內(nèi)部原理。

ThreadLocal原理

為了幫助大家快速的知曉ThreadLocal原理,這里我將ThreadLocal的原理用下圖表示出來了:

threadLocal.png

在上圖中我們可以發(fā)現(xiàn),整個(gè)ThreadLocal的使用都涉及到線程中ThreadLocalMap,雖然我們在外部調(diào)用的是ThreadLocal.set(value)方法,但本質(zhì)是通過線程中的ThreadLocalMap中的set(key,value)方法,那么通過該情況我們大致也能猜出get方法也是通過ThreadLocalMap。那么接下來我們一起來看看ThreadLocal中set與get方法的具體實(shí)現(xiàn)與ThreadLocalMap的具體結(jié)構(gòu)。

ThreadLocal的set方法

在使用ThreadLocal時(shí),我們會(huì)調(diào)用ThreadLocal的set(T value)方法對線程中的私有變量設(shè)置,我們來查看ThreadLocal的set方法

    public void set(T value) {
        Thread t = Thread.currentThread();//獲取當(dāng)前線程
        ThreadLocalMap map = getMap(t);//拿到線程的LocalMap
        if (map != null)
            map.set(this, value);//設(shè)值 key->當(dāng)前ThreadLocal對象。value->為當(dāng)前賦的值
        else
            createMap(t, value);//創(chuàng)建新的ThreadLocalMap并設(shè)值
    }

當(dāng)調(diào)用set(T value) 方法時(shí),方法內(nèi)部會(huì)獲取當(dāng)前線程中的ThreadLocalMap,獲取后進(jìn)行判斷,如果不為空,就調(diào)用ThreadLocalMap的set方法(其中key為當(dāng)前ThreadLocal對象,value為當(dāng)前賦的值)。反之,讓當(dāng)前線程創(chuàng)建新的ThreadLocalMap并設(shè)值,其中g(shù)etMap()與createMap()方法具體代碼如下:

  ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

簡簡單單的通過ThreadLocalMap的set()方法,我們已經(jīng)大致了解了。ThreadLocal為什么能操作線程內(nèi)的私有數(shù)據(jù)了,ThreadLocal中所有的數(shù)據(jù)操作都與線程中的ThreadLocalMap有關(guān),同時(shí)那我們接下來看看ThreadLocalMap相關(guān)代碼。

ThreadLocalMap 內(nèi)部結(jié)構(gòu)

ThreadLocalMap是ThreadLocal中的一個(gè)靜態(tài)內(nèi)部類,官方的注釋寫的很全面,這里我大概的翻譯了一下,ThreadLocalMap是為了維護(hù)線程私有值創(chuàng)建的自定義哈希映射。其中線程的私有數(shù)據(jù)都是非常大且使用壽命長的數(shù)據(jù)(其實(shí)想一想,為什么要存儲(chǔ)這些數(shù)據(jù)呢,第一是為了把常用的數(shù)據(jù)放入線程中提高了訪問的速度,第二是如果數(shù)據(jù)是非常大的,避免了該數(shù)據(jù)頻繁的創(chuàng)建,不僅解決了存儲(chǔ)空間的問題,也減少了不必要的IO消耗)。

ThreadLocalMap 具體代碼如下:

 static class ThreadLocalMap {
        //存儲(chǔ)的數(shù)據(jù)為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 用于存儲(chǔ)數(shù)據(jù)
        private Entry[] table;
        
        //負(fù)載因子,用于數(shù)組容量擴(kuò)容
        private int threshold; // Default to 0
        
        //負(fù)載因子,默認(rèn)情況下為當(dāng)前數(shù)組長度的2/3
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
        //第一次放入Entry數(shù)據(jù)時(shí),初始化數(shù)組長度,定義擴(kuò)容閥值,
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];//初始化數(shù)組長度為16
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);//閥值為當(dāng)前數(shù)組默認(rèn)長度的2/3
        }

從代碼中可以看出,雖然官方申明為ThreadLocalMap是一個(gè)哈希表,但是它與我們傳統(tǒng)認(rèn)識(shí)的HashMap等哈希表內(nèi)部結(jié)構(gòu)是不一樣的。ThreadLocalMap內(nèi)部僅僅維護(hù)了Entry[] table,數(shù)組。其中Entry實(shí)體中對應(yīng)的key為弱引用(下文會(huì)將為什么會(huì)用弱引用),在第一次放入數(shù)據(jù)時(shí),會(huì)初始化數(shù)組長度(為16),定義數(shù)組擴(kuò)容閥值(當(dāng)前默認(rèn)數(shù)組長度的2/3)。

ThreadLocalMap 的set()方法

 private void set(ThreadLocal<?> key, Object value) {

            //根據(jù)哈希值計(jì)算位置
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            
            //判斷當(dāng)前位置是否有數(shù)據(jù),如果key值相同,就替換,如果不同則找空位放數(shù)據(jù)。
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {//獲取下一個(gè)位置的數(shù)據(jù)
                ThreadLocal<?> k = e.get();
            //判斷key值相同否,如果是直接覆蓋 (第一種情況)
                if (k == key) {
                    e.value = value;
                    return;
                }
            //如果當(dāng)前Entry對象對應(yīng)Key值為null,則清空所有Key為null的數(shù)據(jù)(第二種情況)
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //以上情況都不滿足,直接添加(第三種情況)
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)//如果當(dāng)前數(shù)組到達(dá)閥值,那么就進(jìn)行擴(kuò)容。
                rehash();
        }

直接通過代碼理解比較困難,這里直接將set方法分為了三個(gè)步驟,下面我們我們就分別對這個(gè)三個(gè)步驟,分別通過圖與代碼的方式講解。

第一種情況, Key值相同

如果當(dāng)前數(shù)組中,如果當(dāng)前位置對應(yīng)的Entry的key值與新添加的Entry的key值相同,直接進(jìn)行覆蓋操作。具體情況如下圖所示

key值相同情況.png

如果當(dāng)前數(shù)組中。存在key值相同的情況,ThreadLocal內(nèi)部操作是直接覆蓋的。這種情況就不過多的介紹了。

第二種情況,如果當(dāng)前位置對應(yīng)Entry的Key值為null

第二種情況相對來說比較復(fù)雜,這里先給圖,然后會(huì)根據(jù)具體代碼來講解。

對應(yīng)位置Key值為null.png

從圖中我們可以看出來。當(dāng)我們添加新Entry(key=19,value =200,index = 3)時(shí),數(shù)組中已經(jīng)存在舊Entry(key =null,value = 19),當(dāng)出現(xiàn)這種情況是,方法內(nèi)部會(huì)將新Entry的值全部賦值到舊Entry中,同時(shí)會(huì)將所有數(shù)組中key為null的Entry全部置為null(圖中大黃色數(shù)據(jù))。在源碼中,當(dāng)新Entry對應(yīng)位置存在數(shù)據(jù),且key為null的情況下,會(huì)走replaceStaleEntry方法。具體代碼如下:

   private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            //記錄當(dāng)前要清除的位置
            int slotToExpunge = staleSlot;
            
            //往前找,找到第一個(gè)過期的Entry(key為空)
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)//判斷引用是否為空,如果為空,擦除的位置為第一個(gè)過期的Entry的位置
                    slotToExpunge = i;

            //往后找,找到最后一個(gè)過期的Entry(key為空),
            for (int i = nextIndex(staleSlot, len);//這里要注意獲得位置有可能為0,
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //在往后找的時(shí)候,如果獲取key值相同的。那么就重新賦值。
                if (k == key) {
                    //賦值到之前傳入的staleSlot對應(yīng)的位置
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    //如果往前找的時(shí)候,沒有過期的Entry,那么就記錄當(dāng)前的位置(往后找相同key的位置)
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                        
                    //那么就清除slotToExpunge位置下所有key為null的數(shù)據(jù)
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                //如果往前找的時(shí)候,沒有過期的Entry,且key =null那么就記錄當(dāng)前的位置(往后找key==null位置)
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 把當(dāng)前key為null的對應(yīng)的數(shù)據(jù)置為null,并創(chuàng)建新的Entry在該位置上
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            //如果往后找,沒有過期的實(shí)體, 
            //且staleSlot之前能找到第一個(gè)過期的Entry(key為空),
            //那么就清除slotToExpunge位置下所有key為null的數(shù)據(jù)
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

上面代碼看起來比較繁雜,但是大家仔細(xì)梳理就會(huì)發(fā)現(xiàn)其實(shí)該方法,主要對四種情況進(jìn)行了判斷,具體情況如下圖表所示:

TIM截圖20180731110649.png

我們已經(jīng)了解了replaceStaleEntry方法內(nèi)部會(huì)清除key==null的數(shù)據(jù),而其中具體的方法與expungeStaleEntry()方法與cleanSomeSlots()方法有關(guān),所以接下來我們來分析這兩個(gè)方法。看看其的具體實(shí)現(xiàn)。

expungeStaleEntry ()方法

    private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 將staleSlot位置下的數(shù)據(jù)置為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的數(shù)據(jù)
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                //如果key不為null,但是該key對應(yīng)的threadLocalHashCode發(fā)生變化,
                //計(jì)算位置,并將元素放入新位置中。
                    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;//返回最后一個(gè)tab[i]) != null的位置
        }

expungeStaleEntry()方法主要干了三件事,第一件,將staleSlot的位置對應(yīng)的數(shù)據(jù)置為null,第二件,刪除并刪除此位置后對應(yīng)相關(guān)聯(lián)位置key = null的數(shù)據(jù)。第三件,如果如果key不為null,但是該key對應(yīng)的threadLocalHashCode發(fā)生變化,計(jì)算變化后的位置,并將元素放入新位置中。

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;//如果有過期的數(shù)據(jù)被刪除,就返回true,反之false
        }

在了解了expungeStaleEntry()方法后,再來理解cleanSomeSlots()方法就很簡單了。其中第一個(gè)參數(shù)表示開始掃描的位置,第二個(gè)參數(shù)是掃描的長度。從代碼我們明顯的看出。就是簡單的遍歷刪除所有位置下key==null的數(shù)據(jù)。

第三種情況,當(dāng)前對應(yīng)位置為null
沒有數(shù)據(jù)的情況.png

圖上為了方便大家,理解清空上下數(shù)據(jù)的情況,我并沒有重新計(jì)算位置(希望大家注意?。。。?/strong>

看到這里,為了方便大家避免不必要的查閱代碼,我直接將代碼貼出來了。代碼如下。

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
               

從上述代碼其實(shí),大家很明顯的看出來,就是清除key==null的數(shù)據(jù),判斷當(dāng)前數(shù)據(jù)的長度是不是到達(dá)閥值(默認(rèn)沒擴(kuò)容前為INITIAL_CAPACITY *2/3,其中INITIAL_CAPACITY = 16),如果達(dá)到了重新計(jì)算數(shù)據(jù)的位置。關(guān)于rehash()方法,具體代碼如下:

 private void rehash() {
         expungeStaleEntries();

         // Use lower threshold for doubling to avoid hysteresis
         if (size >= threshold - threshold / 4)
                resize();
        }
        
 //清空所有key==null的數(shù)據(jù)
 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);
            }
        }
 //重新計(jì)算key!=null的數(shù)據(jù)。新的數(shù)組長度為之前的兩倍      
 private void resize() {
            //對原數(shù)組進(jìn)行擴(kuò)容,容量為之前的兩倍
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            //重新計(jì)算位置
            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++;
                    }
                }
            }
            //重新計(jì)算閥值(負(fù)載因子)為擴(kuò)容之后的數(shù)組長度的2/3
            setThreshold(newLen);
            size = count;
            table = newTab;
        }

rehash內(nèi)部所有涉及到的方法,我都列舉出來了??梢钥闯鲈谔砑訑?shù)據(jù)的時(shí)候,會(huì)進(jìn)行判斷是否擴(kuò)容操作,如果需要擴(kuò)容,會(huì)清除所有的key==null的數(shù)據(jù),(也就是調(diào)用expungeStaleEntries()方法,其中expungeStaleEntry()方法已經(jīng)介紹了,就不過多描述),同時(shí)會(huì)重新計(jì)算數(shù)據(jù)中的位置。

ThreadLocal的get()方法

在了解了ThreadLocal的set()方法之后,我們看看怎么獲取ThreadLocal中的數(shù)據(jù),具體代碼如下:

  public T get() {
        Thread t = Thread.currentThread();//獲取當(dāng)前線程
        ThreadLocalMap map = getMap(t);//拿到線程中的Map
        if (map != null) {
            //根據(jù)key值(ThreadLocal)對象,獲取存儲(chǔ)的數(shù)據(jù)
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果ThreadLocalMap為空,創(chuàng)建新的ThreadLocalMap 
        return setInitialValue();
    }

其實(shí)ThreadLocal的get方法其實(shí)很簡單,就是獲取當(dāng)前線程中的ThreadLocalMap對象,如果沒有則創(chuàng)建,如果有,則根據(jù)當(dāng)前的 key(當(dāng)前ThreadLocal對象),獲取相應(yīng)的數(shù)據(jù)。其中內(nèi)部調(diào)用了ThreadLocalMap的getEntry()方法區(qū)獲取數(shù)據(jù),我們繼續(xù)查看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()方法內(nèi)部也很簡單,也只是根據(jù)當(dāng)前key哈希后計(jì)算的位置,去找數(shù)組中對應(yīng)位置是否有數(shù)據(jù),如果有,直接將數(shù)據(jù)放回,如果沒有,則調(diào)用getEntryAfterMiss()方法,我們繼續(xù)往下看 。

 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,清除當(dāng)前位置下所有key=null的數(shù)據(jù)。
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;//沒有數(shù)據(jù)直接返回null
        }

從上述代碼我們可以知道,如果從數(shù)組中,獲取的key==null的情況下,get方法內(nèi)部也會(huì)調(diào)用expungeStaleEntry()方法,去清除當(dāng)前位置所有key==null的數(shù)據(jù),也就是說現(xiàn)在不管是調(diào)用ThreadLocal的set()還是get()方法,都會(huì)去清除key==null的數(shù)據(jù)。

ThreadLocal內(nèi)存泄漏的問題

通過整個(gè)ThreadLocal機(jī)制的探索,我相信大家肯定會(huì)有一個(gè)疑惑,為什么ThreadLocalMap中采用是的是弱引用作為Key?關(guān)于該問題,涉及到Java的回收機(jī)制。

為什么使用弱引用

在Java中判斷一個(gè)對象到底是不是需要回收,都跟引用相關(guān)。在Java中引用分為了4類。

  • 強(qiáng)引用:只要引用存在,垃圾回收器永遠(yuǎn)不會(huì)回收Object obj = new Object();而這樣 obj對象對后面new Object的一個(gè)強(qiáng)引用,只有當(dāng)obj這個(gè)引用被釋放之后,對象才會(huì)被釋放掉。
  • 軟引用:是用來描述,一些還有但并非必須的對象,對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對象列進(jìn)回收范圍之中進(jìn)行第二次回收。(SoftReference)
  • 弱引用:也是用來描述非必須的對象,但是它的強(qiáng)度要比軟引用更弱一些。被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前,當(dāng)垃圾收集器工作是,無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉被弱引用關(guān)聯(lián)的對象。(WeakReference)
  • 虛引用:也被稱為幽靈引用,它是最弱的一種關(guān)系。一個(gè)對象是否有引用的存在,完全不會(huì)對其生存時(shí)間構(gòu)成影響,也無法通過一個(gè)虛引用來取得一個(gè)實(shí)例對象。

通過該知識(shí)點(diǎn)的了解后,我們再來了解為什么ThreadLocal不能使用強(qiáng)引用,如果key使用強(qiáng)引用,那么當(dāng)引用ThreadLocal的對象被回收了,但ThreadLocalMap中還持有ThreadLocal的強(qiáng)引用,如果沒有手動(dòng)刪除,ThreadLocal不會(huì)被回收,導(dǎo)致內(nèi)存泄漏。

弱引用帶來的問題

當(dāng)我們知道了為什么采用弱引用來作為ThreadLocalMap中的key的知識(shí)點(diǎn)后,這個(gè)時(shí)候又會(huì)引申出另一個(gè)問題不管是調(diào)用ThreadLocal的set()還是get()方法,都會(huì)去清除key==null的數(shù)據(jù)。為毛我們要去清除那些key==null的Entry呢?

為什么清除key==null的Entry主要有以下兩個(gè)原因,具體如下所示:

  • 從上面我們已經(jīng)知道了,ThreadLocalMap使用ThreadLocal的弱引用作為key,也就是說,如果一個(gè)ThreadLocal沒有外部強(qiáng)引用來引用它,那么系統(tǒng) GC 的時(shí)候,這個(gè)ThreadLocal勢必會(huì)被回收。這樣一來,ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,
  • 如果當(dāng)前線程遲遲不結(jié)束的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread Ref(當(dāng)前線程引用) -> Thread -> ThreadLocalMap -> Entry -> value,那么將會(huì)導(dǎo)致這些Entry永遠(yuǎn)無法回收,造成內(nèi)存泄漏。

通過以上分析,我們可以了解在ThreadLocalMap的設(shè)計(jì)中其實(shí)已經(jīng)考慮到上述兩種情況,也加上了一些防護(hù)措施。(在調(diào)用ThreadLocal的get(),set(),remove()方法的時(shí)候都會(huì)清除線程ThreadLocalMap里所有key為null的Entry)

ThreadLocal使用注意事項(xiàng)

雖然ThreadLocal幫我們考慮了內(nèi)存泄漏的問題,為我們加上了一些防護(hù)措施。但是在實(shí)際使用中,我們還是需要注意避免以下兩種情況,下述兩種情況仍然有可能會(huì)導(dǎo)致內(nèi)存泄漏。

避免使用static的ThreadLocal

使用static修飾的ThreadLocal,延長了ThreadLocal的生命周期,可能導(dǎo)致的內(nèi)存泄漏。具體原因是在Java虛擬機(jī)在加載類的過程中為靜態(tài)變量分配內(nèi)存。static變量的生命周期取決于類的生命周期,也就是說類被卸載時(shí),靜態(tài)變量才會(huì)被銷毀并釋放內(nèi)存空間。而類的生命周期結(jié)束與下面三個(gè)條件相關(guān)。

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

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

其實(shí)理解起來也很簡單,就是第一次調(diào)用了ThreadLocal設(shè)置數(shù)據(jù)后,就不在調(diào)用get()、set()、remove()方法。也就是說現(xiàn)在ThreadLocalMap中就只有一條數(shù)據(jù)。那么如果調(diào)用ThreadLocal的線程一直不結(jié)束的話,即使ThreadLocal已經(jīng)被置為null(被GC回收),也一直存在一條強(qiáng)引用鏈:Thread Ref(當(dāng)前線程引用) -> Thread -> ThreadLocalMap -> Entry -> value,導(dǎo)致數(shù)據(jù)無法回收,造成內(nèi)存泄漏。

總結(jié)

  • ThreadLocal本質(zhì)是操作線程中ThreadLocalMap來實(shí)現(xiàn)本地線程變量的存儲(chǔ)的
  • ThreadLocalMap是采用數(shù)組的方式來存儲(chǔ)數(shù)據(jù),其中key(弱引用)指向當(dāng)前ThreadLocal對象,value為設(shè)的值
  • ThreadLocal為內(nèi)存泄漏采取了處理措施,在調(diào)用ThreadLocal的get(),set(),remove()方法的時(shí)候都會(huì)清除線程ThreadLocalMap里所有key為null的Entry
  • 在使用ThreadLocal的時(shí)候,我們?nèi)匀恍枰⒁猓苊馐褂胹tatic的ThreadLocal,分配使用了ThreadLocal后,一定要根據(jù)當(dāng)前線程的生命周期來判斷是否需要手動(dòng)的去清理ThreadLocalMap中清key==null的Entry。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,157評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內(nèi)容