ThreadLocal原始碼解讀和記憶體洩露分析

z1340954953發表於2018-04-27

什麼是TheadLocal

在多執行緒環境下,每個執行緒可以將自己的私有值儲存到ThreadLocal,使用時從ThreadLocal中取出,起到一個資料隔離,保證執行緒安全的作用。

public
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;

每個執行緒都有一個關聯的ThreadLocalMap,ThreadLocalMap中儲存了多個ThreadLocal-value的鍵值組。

ThreadLocalMap原始碼

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal> {
            Object value;
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        private Entry[] table;

ThreadLocalMap內部維護一個弱引用的Entry[]陣列,存放ThreadLocal - value 的鍵值對組。

ThreadLocal的get方法

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,呼叫getEntry方法獲取value,如果map為空,重新繫結thread和threadLocal

ThreadLocalMap的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);
        }
		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;
        }
private int expungeStaleEntry(int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;
	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) {
			e.value = null;
			tab[i] = null;
			size--;
		} else {
			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;
}

1.根據TheadLocal的hash值,計算出在陣列中下表,找到key相同的entry

2. 否則,向下遍歷找下一個,直到找到ThreadLocal相同的entry

3. 遍歷過程中,如果key為null,將當前的entry回收,並且重新處理Entry陣列,將key為null的Entry清除。

ThreadLocal的set方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
獲取執行緒繫結的ThreadLocalMap,呼叫TheadLocalMap的set方法

ThreadLocalMap的set方法

 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)]) {
                ThreadLocal k = e.get();
                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);//當前索引位置,設定新的entry,並清除過期的entry
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
			//清除過期的entry,並按照原來的兩倍大小擴容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
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;
        }

1. 根據key的hash值,確定到Entry陣列的下標,如果下標在陣列中,並且key相同,就替換

2. 如果當前下標的entry為空,就會新設定一個entry到這個索引位置,並會清除過期的entry

3. 如果下標不在當前陣列中,就新增entry,並清除過期的entry,如沒有過期的entry並且size>=threshold,就會按照兩倍length大小擴容。

ThreadLocal的remove方法

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

獲取執行緒繫結的ThreadLocalMap,呼叫remove方法

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;
                }
            }
        }
public void clear() {
        this.referent = null;
    }

 找到key相同的entry,將引用置null,使得gc回收這個entry,並會清除過期的entry.

注意,ThreadLocal的get/set/remove方法都去清除過期的entry

ThreadLocal導致的記憶體洩露分析


棧中執行緒引用通過強引用指向堆中的Thread物件和ThreadLocalMap(value實際存放在ThreadLocalMap裡面的Entry)

Entry類的原始碼:



獲取到的引用是弱引用,也就是當ThreadLocal被GC回收的時候,entry可能沒有被GC,這就造成了記憶體洩露

那entry什麼時候被回收呢?

只有當current thread終止時候,ThreadLocalMap被gc了,這個entry才會被回收。

如何解決

ThreadLocal的get/set/remove方法都會將過期的entry刪除,只要呼叫其中一個方法就行。

一般的解決方法是,在get到值後,呼叫ThreadLocal的remove方法,將entry主動清除。




相關文章