1. 程式人生 > >執行緒本地變數ThreadLocal

執行緒本地變數ThreadLocal

文章目錄

一、什麼是ThreadLocal

ThreadLocal並不是用來併發控制訪問一個共同物件,而是為了給每個執行緒分配一個只屬於該執行緒的變數,顧名思義它是local variable(執行緒區域性變數)。它的功用非常簡單,就是為每一個使用該變數的執行緒都提供一個變數值的副本,是每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突,實現執行緒間的資料隔離。從執行緒的角度看,就好像每一個執行緒都完全擁有該變數。

二、原始碼實現

set方法實現

public void set(T value) {
    Thread t = Thread.currentThread();//1
    ThreadLocalMap map = getMap(t);//2
    if (map != null)
        map.set(this, value);//4
    else
        createMap(t, value);//3
}
 ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

接下來我們按程式碼行逐行講解:
1、獲取當前執行緒,即t為正在執行ThreadLocal的set方法的那個執行緒
2、獲取ThreadLocalMap,其中ThreadLocalMap為ThreadLocal的一個內部類,後面再詳細解釋,其中getMap()方法引數列表為當前執行緒地址
接下來,我們看一下getMap方法,這個方法只有一句話return t.threadLocals,顯然,threadLocals型別為ThreadLocalMap,是Thread執行緒類的一個成員變數,我們開啟Thread類檢視該變數,發現其初始值為null

ThreadLocal.ThreadLocalMap threadLocals = null;

3、<createMap(t, value)>由於首次執行set方法時,threadLocals變數為null,所以執行程式碼3,即建立一個ThreadLocalMap,接下來我們看一下createMap()這個方法。引數列表包含兩個,分別為當前執行緒和準備儲存的值。實現程式碼只有一行

t.threadLocals = new ThreadLocalMap(this, firstValue);

不難發現,這個方法的實際作用其實就是為了當前執行緒的threadLocals變數做初始化,接下來我們看一下初始化細節new ThreadLocalMap(this, firstValue)。引數列表包含兩個,分別為當前ThreadLocal物件,準備儲存的值。ThreadLocalMap初始化程式碼為

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

ThreadLocalMap其實本質上就是一個弱化版的HashMap,其中維持了一個Entry陣列,Entry陣列的每一維分別為一個連結串列,其中Entry陣列初始長度為INITIAL_CAPACITY=16,接下來看這句程式碼

firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

很顯然,是根據傳入的ThreadLocal物件的HashCode對陣列長度除留取餘,i則是firstValue對應的需要儲存的Entry陣列下標
4、<map.set(this, value)>當map不為null時,將值存入map,具體實現程式碼為

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);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

儲存過程類似HashMap儲存

get方法實現

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

獲取過程為,先獲取當前執行緒,然後獲取當前執行緒持有的ThreadLocalMap,若map不為null,則呼叫ThreadLocalMap的getEntry方法,以ThreadLocal物件為key,獲取對應的Entry物件,並獲取值

remove方法實現

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

根據ThreadLocal物件,移除對應值

三、記憶體洩漏問題

由於每個thread中都持有一個map, map的型別是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal例項. 這個Map使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal例項置為null以後,沒有任何強引用指向threadlocal例項,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收。
所以,只要這個執行緒物件被gc回收,就不會出現記憶體洩露,但在threadLocal設為null和執行緒結束這段時間不會被回收的,就發生了我們認為的記憶體洩露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的。就可能出現記憶體洩露。