執行緒本地變數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和執行緒結束這段時間不會被回收的,就發生了我們認為的記憶體洩露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的。就可能出現記憶體洩露。