ThreadLocal 源碼解析 原

本文將從以下幾個方面介紹

前言

栗子

類圖

ThreadLocal源碼分析

ThreadLocalMap 源碼分析

ThreadLocal 可能會導致內存泄漏

前言

ThreadLocal 顧名思義就是在每個線程內部都會存儲只有當前線程才能訪問的變量的一個副本,然後當前線程修改了該副本的值後而不會影響其他線程的值,各個變量之間相互不影響。

當我們需要共享一個變量,而該變量又不是線程安全的時候,可以使用 ThreadLocal 來複制該變量的一個副本;又比如,瀏覽器用戶的登錄信息需要從當前請求request中獲取,如果需要在很多地方會用到該用戶的登錄信息, 一個解決辦法是向這些所有用到的地方傳遞request參數,另外一個辦法就是利用ThreadLocal, 獲取登錄信息後把它放到當前線程中的ThradLocal變量中,任何需要的時候從當前線程中取就可以了。

栗子

首先看一個不使用 ThreadLocal 的簡單不成熟栗子,每個線程都要修改共享變量 i 的值:

    private int i = 0;

    private void createThread() throws InterruptedException {
        Thread thread = new Thread(() -> {
            i = 0;
            System.out.println(Thread.currentThread().getName() + " : " +  i);
            i+=10;
            System.out.println(Thread.currentThread().getName() + " : " +  i);
        });
        thread.start();
        thread.join();
    }

    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        for (int j = 0; j < 5; j++) {
            m.createThread();
        }

    }
輸出:
Thread-0 : 0
Thread-0 : 10
Thread-1 : 0
Thread-1 : 10
Thread-2 : 0
Thread-2 : 10
Thread-3 : 0
Thread-3 : 10
Thread-4 : 0
Thread-4 : 10

在每個線程修改該共享變量的值之前,都需要重置該變量的值,之後纔會進行修改,這樣結果纔會符合我們的預期。

接下來看下使用 ThreadLocal 是來實現的:

    private int i = 0;
    // 爲每個線程創建變量 i 的副本
    private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> i);

    private void createThread2() throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
            threadLocal.set(threadLocal.get() + 10);
            System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
        });
        thread.start();
        thread.join();
    }

    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        for (int j = 0; j < 5; j++) {
            m.createThread2();
        }
    }

輸出:

Thread-0 : 0
Thread-0 : 10
Thread-1 : 0
Thread-1 : 10
Thread-2 : 0
Thread-2 : 10
Thread-3 : 0
Thread-3 : 10
Thread-4 : 0
Thread-4 : 10

可以看到,使用 ThreadLocal 同樣實現上述的效果,但是不需要再每個線程執行之前重置該共享變量了。

注:使用 join() 方法爲了讓線程順序執行,線程1執行完了線程2再執行

源碼分析

接下來看下 ThreadLocal 的一個實現

類圖:先來看下它的一個類圖

從該類圖中,可以看到,ThreadLocal 並沒有實現任何的類,也沒有實現任何的接口,它只有兩個內部類,ThreadLocalMap SuppliedThreadLocalThreadLocalMap 類中還有一個 Entry 內部類,可以看到,類結構是很簡單的。SuppliedThreadLocal 只是爲了實現 Java 8 的函數式編程(Lambda表達式),可以忽略。關於 Java 8 的 Lambda 可以參考 Lambda表達式  Java 8 中的流--Stream

ThreadLoal 方法

T get() 返回當前線程本地變量的值
protected T initialValue() 初始化當前線程本地變量的值,默認爲null,一般需要重寫該方法
void remove() 刪除不再使用的 ThreadLocal
void set(T value) 設置當前線程本地變量的值
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) 使用Lambda表達式設置初始值,和 initialValue() 作用是一樣的

ThreadLocal 的方法使用都比較簡單,接下來就看看它們是怎麼實現的,

ThreadLocal

public class ThreadLocal<T> {
    // 哈希值
    private final int threadLocalHashCode = nextHashCode();
    
    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    //防止哈希衝突
    private static final int HASH_INCREMENT = 0x61c88647;

    // 當前線程的本地變量的初始值,默認爲null,一般需要重寫該方法
    protected T initialValue() {
        return null;
    }

    // Lambda 方式設置初始值
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    // 構造方法
    public ThreadLocal() {
    }

    // 獲取 ThreadLocalMap 
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    // 根據線程,和變量值創建 ThreadLocalMap
    // 每個線程都在自己的 ThreadLocalMap
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

上述是 ThreadLocal 的一些輔助的方法,主要方法 set , get 方法主要是在 ThreadLocalMap 中實現,所以需要在下面結合 ThreadLocalMap 中來說。

ThreadLocalMap

首先,ThreadLocalMap 是一個自定義的哈希映射,僅僅是用來維護線程本地變量的值,ThreadLocalMap 使用 WeakReferences 作爲鍵,爲了能夠及時的GC,關於 WeakReferences ,可以參考 java虛擬機之初探

ThreadLocalMap 的定義

    static class ThreadLocalMap {

        // 內部類,有兩個屬性:ThreadLocal 和 Object
        // ThreadLocal:作爲key,當key==ull(即entry.get()== null)表示不再引用該鍵,因此可以從表中刪除
        // Object:本地變量的值
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        // Entry數組的初始容量,爲16,必須爲2的冪
        private static final int INITIAL_CAPACITY = 16;

        // Entry數組,可重置大小,數組的長度必須爲2的冪
        private Entry[] table;

        // Entry數組中元素的個數
        private int size = 0;

        //Entry擴容的閾值,默認爲0
        private int threshold; 

        //設置閾值,爲 len 的三分之二
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        // Entry數組的下一個索引
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        // Entry數組的上一個索引
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

     ....方法.........
    }

從上述定義的屬性和類可以看到,ThreadLocalMap 主要使用數組來實現的,數組的每一項是一個 Entry 對象,Entry 對象中會持有當前線程的引用和當前線程所綁定的變量值。結構如下所示:

接下來看下 ThreadLocalMap 方法的實現,在該部分中,需要結合 ThreadLocal 的方法一起來看,

get() 方法

// 返回當前線程所綁定的本地變量值,如果當前線程爲null,則返回setInitialValue()方法中的值
public T get() {
    // 獲取當前線程
	Thread t = Thread.currentThread();
    // 獲取ThreadLocalMap 
	ThreadLocalMap map = getMap(t);
	if (map != null) {
        // 在 ThreadLocalMap 中獲取當前線程對應的Entry,Entry 中存儲了當前線程所綁定的本地變量的值
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
            // 獲取當前線程所綁定的本地變量的值,並返回
			T result = (T)e.value;
			return result;
		}
	}
    // 如果當前線程沒有的 ThreadLocalMap 中,則返回 setInitialValue 中的值
	return setInitialValue();
}

get() 方法不要有以下幾步:

1.獲取當前線程

2.獲取線程內的 ThreadLocalMap,如果map已經存在,則以當前的ThreadLocal爲鍵,獲取Entry對象,並從從Entry中取出值

3.如果 map 不存在,則調用setInitialValue方法執行初始化

現在,來看下如果從 ThreadLocalMap中獲取當前線程所對應的 Entry 對象:

getEntry() 方法如下

private Entry getEntry(ThreadLocal<?> key) {
    // 獲取對應線程的hashcode
    // 計算 Entry數組的索引
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];
    // 如果該索引處的Entry對象剛好等於key,則直接返回
	if (e != null && e.get() == key)
		return e;
	else
    // 如果上述條件不滿足,則進入 getEntryAfterMiss 方法
		return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss()方法

該方法主要是,當在當前的索引中找不到對應的 Entry 對象時執行,在該方法內部,主要是在 Entry 數組中循環查找對應key,如果key爲空,則進行清理操作

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    // 當前 Entry數組
	Entry[] tab = table;
	int len = tab.length;
    // 如果當前Entry數組 i 對應的位置的Entry對象不爲空 
	while (e != null) {
		ThreadLocal<?> k = e.get();
        // 如果key等於Entry數組 i 對應的位置的Entry對象,則直接返回
		if (k == key)
			return e;
		if (k == null)
            // 如果 Entry 數組 i 對應的位置的 Entry 對象爲空,則刪除該 Entry 對象,resize Entry數組
			expungeStaleEntry(i);
		else
           // 否則,獲取 Entry 數組的下一個索引位置,繼續查找
			i = nextIndex(i, len);
		e = tab[i];
	}
	return null;
}

expungeStaleEntry()方法

當在 Entry 數組中對應的位置不存在任何引用的時候,進行 Entry 數組的清理操作,resize Entry 數組:

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

	// 把當前索引對應位置的對象設置爲null
	tab[staleSlot].value = null;
	tab[staleSlot] = null;
	size--; // Entry數組大小減1

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

在執行完上述方法後,get() 方法就會得到一個 Entry 對象,之後返回該對象的value,該value就是當前線程所綁定的本地變量的值。

在上面所說的 get() 方法中,如果 ThreadLocalMap 不存在,則執行 setInitialValue 進行初始化,下面看下setInitialValue:

setInitialValue()方法

private T setInitialValue() {
    // 調用 initialValue 方法,該方法默認返回null,一般需要重寫該方法
	T value = initialValue();
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
    // 如果 ThreadLocalMap 已存在,則設置初值爲 initialValue 方法的返回值
	if (map != null)
		map.set(this, value);
	else
        // 如果 ThreadLocalMap 不存在,則創建
		createMap(t, value);
	return value;
}

ThreadLocalMap.set()方法

ThreadLocalMap 的 set 方法,主要用來設置其對應的值:

private void set(ThreadLocal<?> key, Object value) {
	// 當前的Entry數組
	Entry[] tab = table;
	int len = tab.length;
    // 數組索引
	int i = key.threadLocalHashCode & (len-1);
    // 遍歷 Entry 數組
	for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
		ThreadLocal<?> k = e.get();
        // 如果在 Entry 找到,則設置value
		if (k == key) {
			e.value = value;
			return;
		}
        // 如果當前的ThreadLocal爲空,則調用replaceStaleEntry來更換這個key爲空的Entry
		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
	}
    // 如果在 Entry 數組中沒有找到對應的key ,則創建,插入到數組中
	tab[i] = new Entry(key, value);
	int sz = ++size;
    // 清理 Entry 數組中爲null的項,且如果數組大小大於等於我們設置的閾值,則rehash數組
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

cleanSomeSlots 方法裏面還是會調用上面所說的 expungeStaleEntry 方法進行清理 Entry數組爲null的項。

rehash()方法

如果當Entry數組的大小大於等於設置的閾值的話,Entry數組就需要進行擴容操作:

private void rehash() {
    // 清空Entry數組
	expungeStaleEntries();
    // 如果 數組大小大於等於 閾值的 3/4,則擴容
	if (size >= threshold - threshold / 4)
        // 擴容
		resize();
}

expungeStaleEntries()方法

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

resize()方法

把 Entry數組的容量擴大爲原來的 2 倍:

private void resize() {
    // 舊的數組
	Entry[] oldTab = table;
    // 舊數組的長度
	int oldLen = oldTab.length;
    // 新的數組的長度爲舊的的2倍
	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;
}

ThreadLocal的 set() 方法

ThreadLocal 的 set 方法用來設置當前線程所綁定的變量的值,它的實現和setInitialValue差不多,:

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
    // 如果 ThreadLocalMap  存在,則設置值
	if (map != null)
		map.set(this, value);
	else
        // 如果 ThreadLocalMap  不存在則創建
		createMap(t, value);
}

ThreadLocal 的remove() 方法

 public void remove() {
	 ThreadLocalMap m = getMap(Thread.currentThread());
	 if (m != null)
         // 調用 ThreadLocalMap 的 remove 方法
		 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; e = tab[i = nextIndex(i, len)]) {
		if (e.get() == key) {
			e.clear();
			expungeStaleEntry(i);
			return;
		}
	}
}

以上就是 ThreadLocal 的一個實現過程。

ThreadLocal 可能會導致內存泄漏

 從上面的代碼中可以看到,ThreadLocalMap 使用 ThreadLocal 的弱引用作爲key,如果一個 ThreadLocal 沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal 就會被回收,這樣一來,ThreadLocalMap 中就會出現 key 爲 null 的 Entry,就沒有辦法訪問這些key爲null的Entry的value,如果當前線程再遲遲不結束的話,這些key爲null的Entry的value永遠無法回收,造成內存泄漏。在 ThreadLocal 中 的 get, set 和remove 方法中,都對 Entry 的key進行的null的判斷,如果爲null,則會 expungeStaleEntry 進行清理操作;

所以,在線程中使用完 ThreadLocal 變量後,要記得及時remove掉。

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