併發編程-ThreadLocal解析

       首先,在解析ThreadLocal之前,我們首先要知道這東西是個什麼玩意兒,ThreadLocal類顧名思義可以理解爲線程本地變量。也就是說如果定義了一個ThreadLocal,每個線程往這個ThreadLocal中讀寫是線程隔離,互相之間不會影響的。它提供了一種將可變數據通過每個線程有自己的獨立副本從而實現線程封閉的機制。

      下面,我來手寫一個ThreadLocal的實現來理解一下它的原理:

public class ThreadLocal<T>
{
    private Map<Thread,T> threadMap = new ConcurrentHashMap<Thread,T>();

    public void set(T value)
    {
        Thread thread = Thread.currentThread();
        this.threadMap.put(thread,value);
    }

    public T get()
    {
        Thread thread = Thread.currentThread();
        if(this.threadMap.containsKey(thread))
        {
            return this.threadMap.get(thread);
        }
        return null;
    }

    public void remove()
    {
        Thread thread = Thread.currentThread();
        if( this.threadMap.containsKey(thread))
        {
            this.threadMap.remove(thread);
        }
    }

}

看了上邊的代碼,我們可以看出,ThreadLocal其實就是一個類似於map的存儲結構,只不過在源碼中,它是一個線程私有的局部變量,而不是咱們定義的threadMap。

 

源碼解析

那麼我們對照上邊自己寫的ThreadLocal,來看看JDK源碼裏是怎麼實現的:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

獲取Thread實例,然後將ThreadLocal實例作爲key放入一個map中

我們再來看看getMap()這個方法

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

實際上getMap方法就是返回的Thread的一個成員變量,那麼這個成員變量是一個ThreadLocalMap類型的對象,我們再來看看

ThreadLocalMap是個什麼東西

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

那麼ThreadLocalMap的底層是一個Entry的靜態類數組,實現了一種鍵值對的數據結構,我們常用Map的底層也是這種數據結構吧,無非就是這裏的key值是一個弱引用,這個我們呆會兒再講,那麼現在我們是不是就可以畫出ThreadLocal的數據結構了?

 

        那麼我們在使用ThreadLocal的set方法時,它首先會獲取當前線程的threadLocals這個成員變量,如果map不爲空,則將當前ThreadLocal實例+value放入Entry中,一個threadLocals有N個Entry,所以一個線程中可以new多個ThreadLocal實例來存放不同數據,一個ThreadLocal實例就對應map中一個key,當然你也可以把value打包成一個map,然後用一個ThreadLocal實現也是莫得問題滴。

       如果獲取的線程map爲空,那麼set方法會調用createMap方法

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

    private static final int INITIAL_CAPACITY = 16;
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

createMap就是創建一個初始大小爲16的Entry數組,通過用firstKey的threadLocalHashCode與初始大小16取模得到哈希值,得到數組的下標位置,然後初始化對應下標的Entry對象,setThreshold(INITIAL_CAPACITY)就是說設置擴容閾值爲容量的三分之二,擴容數是原來的2倍,擴容方法是resize()。那麼int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);這段代碼的意思就是說計算的下標值可以更均勻的分佈在map中,擴容是2的冪次方也是更加能優化性能,因爲ThreadLocalMap使用線性探測法來解決散列衝突,所以實際上Entry[]數組在程序邏輯上是作爲一個環形存在的。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

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

這個是get方法,其實就是跟set的邏輯差不多,最終是從Entry數組中取到對應得key值,經過運算,如果key值能直接命中,則返回value,否則使用線性探測法來尋找key值

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            //獲取當前ThreadLocalMap中的數組
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                //再次判斷是否命中key值,若命中則返回
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
             //該entry對應的ThreadLocal已經被回收,調用expungeStaleEntry來清理無效的entry,這個跟弱引用有關
                    expungeStaleEntry(i);
                else
                    //進行線性尋址
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

nextIndex的意思就是說從當前下標往下依次查找,如果Entry[i]的key沒有命中,則找Entry[i+1],直到找到Entry[INITIAL_CAPACITY-1],如果還沒有就返回null。

public void remove() {
         //得到當前線程的ThreadLocalMap 
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             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;
                 //採用線性尋址法,依次判斷是否命中key值
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    //清理弱引用,將Entry的key值置null
                    e.clear();
                    //執行清理
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

remove方法尋找對應key值的方式和getEntry是一樣的,最後就是清理一下。

 

ThreadLocal內存泄漏分析

上邊我們有看到,Entry對象的key值是一個弱引用,這就涉及到了ThreadLocal內存泄漏的問題,什麼是內存泄漏?內存泄漏就是在程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費。

看下這個示意圖,虛線部分就是一個弱引用,ThreadLocalRef和CurrentThreadRef是線程私有棧中對ThreadLocal實例和Thread實例的引用,這是一個強引用,ThreadLocal和CurrentThred則是代碼中new ThreadLocal實例和當前線程的實例,Map則是線程實例的threadLocals成員變量,也就是上邊咱們說的ThreadLocalMap,那麼什麼是否會發生內存泄漏呢?

當ThreadLocal用完了之後,1引用就斷了,此時jvm進行GC回收的時候,弱引用的對象ThreadLocal就被回收了,也就是說此時,這個Entry實例的key值沒有了,這是一個無效的Entry實例了,但是3,4,5這個引用鏈還是強引用,沒有斷,也就是說此時這個無效的Entry實例無法即時被GC回收,這樣就造成了一種內存泄漏。

爲了解決這種問題,ThreadLocal有一套自我清理的機制,當然這純粹是開發JDK的大佬們在給我們不規範的寫代碼擦屁股,如果我們ThreadLocal調用完成,即時用remove方法清理,就不會存在內存泄漏問題。那麼自我清理機制是什麼呢?小夥伴們可以去看看源碼,跟蹤一下,get,set,remove方法,邏輯裏都會有expungeStaleEntry(int staleSlot)這個方法,這個就是大佬們提供的清理Entry中的value的方法:

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

            // expunge entry at staleSlot
            //首先清理數組下標對應的key/value信息
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            //重點:會根據線性尋址法,依次獲取當前下標之後的Entry,如果Entry的key值爲null,則清理value
            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;
        }

expungeStaleEntry方法就是儘量的清除掉無效的value,但是除了remove能夠百分百調用這個方法外,get和set方法都不一定能夠即時調用這個方法來清理無效Entry,所以還是會存在內存泄漏的情況,當然線程如果被回收,那麼數組中所有的東東也就被回收掉了,當然如果是線程池+ThreadLocal的情況就是大寫的杯具了!

從表面上看內存泄漏的根源在於使用了弱引用,但是另一個問題也同樣值得思考:爲什麼使用弱引用而不是強引用?
下面我們分兩種情況討論:
key 使用強引用:對ThreadLocal 對象實例的引用被置爲null 了,但是ThreadLocalMap 還持有這個ThreadLocal 對象實例的強引用,如果沒有手動刪除,ThreadLocal 的對象實例不會被回收,導致Entry 內存泄漏。
key 使用弱引用:對ThreadLocal 對象實例的引用被被置爲null 了,由於ThreadLocalMap 持有ThreadLocal 的弱引用,即使沒有手動刪除,ThreadLocal 的對象實例也會被回收。value 在下一次ThreadLocalMap 調用set,get,remove 都
有機會被回收。
比較兩種情況,我們可以發現:由於ThreadLocalMap 的生命週期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障。
因此,ThreadLocal 內存泄漏的根源是:由於ThreadLocalMap 的生命週期跟Thread 一樣長,如果沒有手動刪除對應key 就會導致內存泄漏,而不是因爲弱引用。

ThreadLocal線程不安全

兄弟,看見這個標題,你是不是懵逼了?ThreadLocal不就是來解決線程不安全性問題的嗎?

如果ThreadLocal正確使用的話,肯定是能夠保證線程安全的,但是執行下以下代碼:

public class ThreadLocalUnsafe implements Runnable {

    public Number number = new Number(0);

    public void run() {
        //每個線程計數加一
        number.setNum(number.getNum()+1);
      //將其存儲到ThreadLocal中
        value.set(number);
        SleepTools.ms(2);
        //輸出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();
        }
    }

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

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

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

}

這種情況下,ThreadLocalMap保存的是Number對象的一個引用,那麼只要這個對象裏的num變化了,是不是所有的ThreadLocalMap中的Number的num都改動了,這種情況就造成了線程不安全了。如果要把它改成線程安全的,則只需要將number對象放入前,new一下就可以了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章