ThreadLocal解析以及內(nèi)存泄露原因、線程不安全情況

ThreadLocal和Synchonized都用語解決多線程并發(fā)訪問的,可以ThreadLocal與Synchonzied有本質(zhì)的差別,synchoronized是利用鎖的機(jī)制,使變量或代碼塊僅僅能被一個(gè)線程訪問。而ThreadLoacal為每個(gè)線程都提供了變量的副本,使得每個(gè)線程在某個(gè)時(shí)間訪問到,這樣對線程間的數(shù)據(jù)進(jìn)行了隔離。

圖片1.png

在Android開發(fā)中Looper是通過ThreadLocal對looper和不同線程進(jìn)行隔離,使每個(gè)線程中都存在一個(gè)Looper。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

ThreadLocal的使用

ThreadLocal類接口很簡單,只有4個(gè)方法。

void set(Object value) 

設(shè)置當(dāng)前線程的線程局部變量的值。

public Object get() 

該方法返回當(dāng)前線程所對應(yīng)的線程局部變量。

public void remove()

將當(dāng)前線程局部變量的值刪除,目的是為了減少內(nèi)存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當(dāng)線程結(jié)束后,對應(yīng)該線程的局部變量將自動被垃圾回收,所以顯式調(diào)用該方法清除線程的局部變量并不是必須的操作,但它可以加快內(nèi)存回收的速度。

protected Object initialValue()

返回該線程局部變量的初始值,該方法是一個(gè)protected的方法,顯然是為了讓子類覆蓋而設(shè)計(jì)的。這個(gè)方法是一個(gè)延遲調(diào)用方法,在線程第1次調(diào)用get()或set(Object)時(shí)才執(zhí)行,并且僅執(zhí)行1次。ThreadLocal中的缺省實(shí)現(xiàn)直接返回一個(gè)null。

ThreadLocal解析

 /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
ThreadLocal.ThreadLocalMap threadLocals = null;

上面先取到當(dāng)前線程,然后調(diào)用getMap方法獲取對應(yīng)的ThreadLocalMap,ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類,然后Thread類中有一個(gè)這樣類型成員,所以getMap是直接返回Thread的成員。

/**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
static class 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 static final int INITIAL_CAPACITY = 16;
        
        private Entry[] table;
}

看下ThreadLocal的內(nèi)部類ThreadLocalMap源碼:
可以看到有個(gè)Entry內(nèi)部靜態(tài)類,它繼承了WeakReference,總之它記錄了兩個(gè)信息,一個(gè)是ThreadLocal<?>類型,一個(gè)是Object類型的值。getEntry方法則是獲取某個(gè)ThreadLocal對應(yīng)的值,set方法就是更新或賦值相應(yīng)的ThreadLocal對應(yīng)的值。

        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);
        }

ThreadLocal中的get方法,其實(shí)就是拿到每個(gè)線程獨(dú)有的ThreadLocalMap
然后再用ThreadLocal的當(dāng)前實(shí)例,拿到Map中的相應(yīng)的Entry,然后就可以拿到相應(yīng)的值返回出去。當(dāng)然,如果Map為空,還會先進(jìn)行map的創(chuàng)建,初始化等工作。

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        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)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

利用getEntryAfterMiss方法找到Entry或者調(diào)用expungeStaleEntry方法處理key為null。這里nextIndex去處理當(dāng)前hash沖突。

Hash沖突怎么解決

與Hashmap不同,ThreadLocal用的是一種線性探測的方式處理,根據(jù)hashcode值確定元素在table數(shù)組中的位置,如果發(fā)現(xiàn)這個(gè)位置已經(jīng)有其他元素和位置沖突就存放下一個(gè)為空的位置。

private void set(ThreadLocal<?> key, Object value) {
    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)]) {
             ...
}...
        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }
        

擴(kuò)容操作

    private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }
        
        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++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }
        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

當(dāng)數(shù)據(jù)超過閾值的四分之三時(shí),擴(kuò)容2倍,閾值設(shè)置為長度的三分之二。

引發(fā)的內(nèi)存泄漏分析

我們可以知道每個(gè)Thread 維護(hù)一個(gè) ThreadLocalMap,這個(gè)映射表的 key是ThreadLocal實(shí)例本身,value 是真正需要存儲的 Object,也就是說 ThreadLocal 本身并不存儲值,它只是作為一個(gè) key 來讓線程從ThreadLocalMap獲取value。


2.png

圖中的虛線表示弱引用。
這樣,當(dāng)把threadlocal變量置為null以后,沒有任何強(qiáng)引用指向threadlocal實(shí)例,所以threadlocal將會被gc回收。這樣一來,ThreadLocalMap中就會出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而這塊value永遠(yuǎn)不會被訪問到了,所以存在著內(nèi)存泄露。
只有當(dāng)前thread結(jié)束以后,current thread就不會存在棧中,強(qiáng)引用斷開,Current Thread、Map value將全部被GC回收。最好的做法是不在需要使用ThreadLocal變量后,都調(diào)用它的remove()方法,清除數(shù)據(jù)。 調(diào)用remove()方法最佳時(shí)機(jī)是線程運(yùn)行結(jié)束之前的finally代碼塊中調(diào)用,這樣能完全避免操作不當(dāng)導(dǎo)致的內(nèi)存泄漏,這種主動清理的方式比惰性刪除有效。

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

            // expunge entry at staleSlot
            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;
                 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;

                        // 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;
        }

這個(gè)方法在ThreadLocal的set、get、remove時(shí)都會被調(diào)用,從上面代碼中,可以看出先清理指定的Entry,再遍歷,如果發(fā)現(xiàn)有Entry的key為null,就清理。Key==null,也就是ThreadLocal對象是null。所以當(dāng)程序中,將ThreadLocal對象設(shè)置為null,在該線程繼續(xù)執(zhí)行時(shí),如果執(zhí)行另一個(gè)ThreadLocal時(shí),就會觸發(fā)該方法。就有可能清理掉Key是null的那個(gè)ThreadLocal對應(yīng)的值。所以說expungStaleEntry()方法清除線程ThreadLocalMap里面所有key為null的value。

        /**
         * Remove the entry for key.
         */
        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的實(shí)現(xiàn),我們可以看見,無論是get()、set()在某些時(shí)候,調(diào)用了expungeStaleEntry方法用來清除Entry中Key為null的Value,但是這是不及時(shí)的,也不是每次都會執(zhí)行的,所以一些情況下還是會發(fā)生內(nèi)存泄露。只有remove()方法中顯式調(diào)用了expungeStaleEntry方法。

static class Entry extends WeakReference<ThreadLocal> 

由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal的對象實(shí)例也會被回收。value在下一次ThreadLocalMap調(diào)用set,get,remove都有機(jī)會被回收。所以說jvm利用弱引用來避免內(nèi)存泄露,通過remove方法回收弱引用。

小結(jié)

key 使用強(qiáng)引用:引用ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用,如果沒有手動刪除,ThreadLocal的對象實(shí)例不會被回收,導(dǎo)致Entry內(nèi)存泄漏。
key 使用弱引用:引用的ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal的對象實(shí)例也會被回收。value在下一次ThreadLocalMap調(diào)用set,get,remove都有機(jī)會被回收

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

使用線程池+ ThreadLocal時(shí)要小心,因?yàn)檫@種情況下,線程是一直在不斷的重復(fù)運(yùn)行的,如果沒有及時(shí)的清理,那么之前對該線程的使用,就會影響到后面的線程了,從而也就造成了value可能造成累積的情況,所以要調(diào)用remove()方法及時(shí)清除來解決。

錯(cuò)誤使用ThreadLocal導(dǎo)致線程不安全

public class ThreadLocalUnsafe implements Runnable {

    public static Number number = new Number(0);

    public void run() {
        //每個(gè)線程計(jì)數(shù)加一
        number.setNum(number.getNum() + 1);
      //將其存儲到ThreadLocal中
        value.set(number);
        //輸出num值
        System.out.println(Thread.currentThread().getName() + "=" + value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
    }
    
}

public class Number {
    
    private int num;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "Number [num=" + num + "]";
    }
    
}

代碼中的數(shù)據(jù)會發(fā)生線程不安全的現(xiàn)象(輸出的全是5),static修飾的類在JVM中只保存一個(gè)實(shí)例對象,ThreadLocalMap中保存的其實(shí)是對象的一個(gè)引用,這樣的話,當(dāng)有其他線程對這個(gè)引用指向的對象實(shí)例做修改時(shí),其實(shí)也同時(shí)影響了所有的線程持有的對象引用所指向的同一個(gè)對象實(shí)例,這樣的話會導(dǎo)致每個(gè)線程輸出的內(nèi)容一致,而上面的程序要正常的工作,應(yīng)該的用法是讓每個(gè)線程中的ThreadLocal都應(yīng)該持有一個(gè)新的Number對象。

總結(jié)

ThreadLocal是解決線程安全的一個(gè)很好的思路,它通過為每個(gè)線程提供了一個(gè)獨(dú)立的變量副本解決了額變量并發(fā)訪問的沖突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機(jī)制解決線程安全問題更簡單,更方便,且結(jié)果程序擁有更高的并發(fā)性。ThreadLocal和synchronize用一句話總結(jié)就是一個(gè)用存儲拷貝進(jìn)行空間換時(shí)間,一個(gè)是用鎖機(jī)制進(jìn)行時(shí)間換空間。
本文由本人編寫總結(jié),版權(quán)由享學(xué)課堂所有

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 12月過去一個(gè)星期,翻過這個(gè)月,我們就要與2017說再見并且迎接新的2018年,感嘆一句時(shí)間一年一年過得真快...
    熙熙Breathe閱讀 328評論 0 3
  • 姓名:符小連 公司:海南蔚藍(lán)時(shí)代實(shí)業(yè)有限公司 組別:第 378 期 努力1組 (參加培訓(xùn)時(shí)的組名) 【日精進(jìn)打卡第...
    AMee_a929閱讀 103評論 0 0
  • 作家格拉德威爾在《異類》書中說: “人們眼中的天才之所以卓越非凡, 并非天資超人一等, 而是付出了持續(xù)不斷的努力。...
    羅東龍閱讀 729評論 1 3

友情鏈接更多精彩內(nèi)容