全域性變數,區域性變數 ,內建函式,全集
什麼是ThreadLocal?
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one has its own, independently initialized copy of the variable.
threadLocal是用於執行緒內部儲存的類,通過threadLocal可以實現執行緒獨享的儲存空間。不同於執行緒同步的概念,threadLocal是讓每個執行緒用於獨立的資料。
使用示例
示例程式碼:
public class ThreadLocalExample { static final ThreadLocal<Human> holder = new ThreadLocal<>(); public static void main(String[] args) { Thread t1 = new Thread(()->{ holder.set(new Human("張三", 21, 0)); System.out.println(Thread.currentThread().getName() + ":" + holder.get()); }, "t1"); Thread t2 = new Thread(()->{ holder.set(new Human("李四", 22, 0)); System.out.println(Thread.currentThread().getName() + ":" + holder.get()); }, "t2"); t1.start(); t2.start(); try{ t1.join(); t2.join(); System.out.println(Thread.currentThread().getName() + ":" + holder.get()); }catch (Exception e){ e.printStackTrace(); } } }
執行結果:
t2:Human{name='李四', age=22, gender=0}
t1:Human{name='張三', age=21, gender=0}
main:null
可以發現不同的執行緒使用同一個threadLocal物件的get方法獲得的結果是不一樣的。這就表示這是資料是線上程中單獨儲存的。
實現原理
set(T value)
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
從set方法可以發現value實際上是儲存在一個叫ThreadLocalMap的物件中的,而這個物件是與當前執行緒關聯的。set方法會先拿到這個map,然後將this和value作為鍵值對儲存。注意,這裡的鍵值對的key是threadLocal物件本身。
如果map為null的話,會先建立map並新增第一個鍵值對。
getMap(Thread t)
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
threadLocal是從執行緒物件獲取到的map物件。
在Thread類中這個map物件定義如下:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
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();
}
與set相同,get同樣是從執行緒物件獲取到的map。然後用threadLocal物件作為key來獲取entry物件,這裡的entry物件與HashMap的entry類似,記錄了一個鍵值對。
可以發現,如果執行緒的map為null時,會進行一次初始化。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
建立map
通過set和get的分析,threadLocalMap採用了一種懶漢式的初始化方法,threadLocalMap的建立其實是在第一次set或get的時候完成的。
ThreadLocalMap
通過對threadLocal的get、set分析,我們發現這兩個方法都是用threadLocal物件作為key線上程物件所擁有的ThreadLocalMap物件上做鍵值對操作。接下來就來看看ThreadLocalMap是什麼。
資料結構之 Hash表
Hash表是使用hashcode作為陣列下標來存放資料的資料結構。它會先計算出物件的hashcode,然後根據hashcode找到槽位(slot)放入。訪問時會計算hashcode然後直接根據下標找到物件。這樣使得訪問的時間複雜度變成了O(1)。
因為hash演算法,可能會出現不同物件擁有相同hashcode 的場景,明顯衝突時你不能直接替換槽位裡現有的物件。所以需要解決hash衝突的方法:
- 拉鍊法:拉鍊法是在槽位中構建連結串列,這樣一個槽位就可以存放多個hash衝突的物件。不過因為連結串列訪問時間複雜度位O(N),這種方式降低了訪問效率。java中的HashMap等都採用了拉鍊法。
- 開放地址法:開放地址法是在衝突後,從衝突位置向後尋找空位放入。這種方式實現簡單,但是使訪問和新增效率都降低了。ThreadLocalMap使用的是這種方式。
Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry繼承了WeakReference,代表Entry物件是一個弱引用。弱引用不管記憶體空間是否充足,只要發生垃圾回收就會被清除。至於使用弱引用的原因,我會在後面關於記憶體洩漏的解決中解釋。
- 強引用:使用new建立的物件都是強引用,強引用不會被垃圾回收
- 軟引用:使用SoftReference建立軟引用,如果一個物件只有軟引用指向,在記憶體不足時觸發的垃圾回收中會被回收
- 弱引用:使用WeakReference建立弱引用,只有弱引用的物件只要發生垃圾回收就會被回收
- 虛引用:虛引用主要用來跟蹤物件被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列 (ReferenceQueue)聯合使用。
除了弱引用,還可以發現entry不同於HashMap的Entry,它只有value屬性,沒有key屬性。這是因為ThreadLocalMap是使用threadLocal作為key,這裡的key-value關係體現在弱引用是引用的ThreadLocal型別。即這個弱引用本身就是key。
set(ThreadLocal key, T value)
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
// 用threadLocal物件的hashcode計算陣列槽位
int i = key.threadLocalHashCode & (len-1);
// 從i位置開始,尋找空槽位
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key相同,替換值
if (k == key) {
e.value = value;
return;
}
// key為null,是髒entry
if (k == null) {
// 替換髒entry
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
nt sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap的set方法不同於HashMap的set方法,它不涉及連結串列或紅黑樹,因為它是使用開放地址法解決的hash衝突。
之前說過ThreadLocalMap使用ThreadLocal物件作為key,所以第一步會從ThreadLocal物件獲取到hashcode並計算出在陣列中的下標。
因為使用的是開放地址發解決hash衝突,所以會從這個計算出的下標開始遍歷陣列。
nextIndex(int i, int len)
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
當下標超過len的時候回到0從頭開始
getEntry(ThreadLocal<?> key)
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// key命中
if (e != null && e.get() == key)
return e;
else
// 沒有命中,通過線性探測法向後尋找
return getEntryAfterMiss(key, i, e);
}
getEntry首先會用ThreadLocal物件的hashcode計算出陣列下標,然後判斷該下標的entry是否命中。沒有命中的話會採用線性探測法向後尋找。
getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 從i位置開始線性探測
while (e != null) {
ThreadLocal<?> k = e.get();
// key命中
if (k == key)
return e;
// 發現k為null的髒entry
if (k == null)
expungeStaleEntry(i);
// 向後遍歷
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
getEntryAfterMiss是getEntry中沒有命中後的線性探測。它會從i位置開始向後搜尋,如果有命中則返回value,如果找到key為null的值會視其為髒entry而清理掉。
記憶體洩漏
什麼是記憶體洩漏
用人話說,記憶體洩漏就是一塊記憶體無法被訪問到,但是又沒有被回收清除。比如以下程式碼中HashMap的記憶體洩漏:
static HashMap<Object, Object> map = new HashMap<>(16);
public static void main(String[] args) {
memoryLeak();
// 現在如何訪問到v1?
}
private static void memoryLeak(){
Object k1 = new Object();
Object v1 = new Object();
map.put(k1, v1);
}
因為k1的作用域只有memoryLeak這個方法,方法結束後我們將無法訪問到v1物件,但是v1物件因為HashMap的引用並沒有被回收,這就導致了一個無法訪問的記憶體空間,也就是記憶體洩漏。
ThreadLocalMap如何解決記憶體洩漏
ThreadLocal的記憶體洩漏
ThreadLocal的整個引用關係如圖,可以發現ThreadLocal的物件是具有一個強引用的,我們通過這個引用去訪問entry。如果這個引用斷開了,這個entry將無法被訪問到,但是有因為它被ThreadLocalMap強引用,所以沒被回收,導致了記憶體洩漏。
弱引用
static class Entry extends WeakReference<ThreadLocal<?>>
之前有提到ThreadLocalMap的Entry繼承了WeakReference。之所以使用WeakReference就是在一定程度上解決記憶體洩漏。
- WeakReference通過get()方法獲取它引用的物件,如果該物件已經被回收了就會返回null。
- ThreadLocalMap通過ThreadLocal物件訪問value,如果我們沒有ThreadLocal物件就訪問不到value,也就導致了記憶體洩漏
- 可以知道弱引用被回收後get()返回null,那麼當我們沒有ThreadLocal物件的引用時,ThreadLocal物件只有弱引用,它將被回收,此時get()返回null
綜上,如果ThreadLocal物件被回收了,那麼Entry的弱引用會返回null。這樣只要發現null,我們就將這個Entry稱為髒entry(StaleEntry),即發生了記憶體洩漏的entry,需要即時清理。接下來就介紹何時發現null,又如何清理。
expungeStaleEntry
getEntry時的清理
ThreadLocal<?> k = e.get();
// key命中
if (k == key)
return e;
// 發現k為null的髒entry
if (k == null)
// 清理髒entry
expungeStaleEntry(i);
getEntry的時候,當發現e.get()為null是會呼叫expungeStaleEntry清理髒entry。具體清理過程如下:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// part I
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// part II
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;
// 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;
}
我們將清理的程式碼分為Part I,II兩個部分。
- Part I:這一部分程式碼將該下標的entry的value設定成了null,這樣使得value物件沒有引用指向可以被回收。最後將該下標設為null,變成了空位。
- Part II:這部分程式碼是在將後面的entry前移,因為現在清楚了髒entry就多出了一個空位,所以要將後面的非空entry向前移動。這樣做的好處是避免了用hashcode沒有命中entry時向後線性探測遇到null結束。
replaceStaleEntry
set()時清理髒entry
ThreadLocal<?> k = e.get();
// key相同,替換值
if (k == key) {
e.value = value;
return;
}
// key為null,是髒entry
if (k == null) {
// 替換髒entry
replaceStaleEntry(key, value, i);
return;
}
與getEntry時相似,也是線上性探測的過程中發現了k==null觸發的清理。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// ---------------- Part I -----------------------------------
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// ---------------- Part II -----------------------------------
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
同樣,我們把程式碼分為兩部分。
- Part I:這一部分程式碼的作用是從當前髒entry位置向前找到最靠前的髒entry,至於具體的原因這裡涉及到了垃圾回收器的工作原理,所以不多介紹
- Part II:依然是使用線性探測法向後遍歷,當找到目標的key後會把目標key和髒entry交換
總結(*)
ThreadLocalMap使用了弱引用和線性探測的方法避免記憶體洩漏。不過這並不能完全避免記憶體洩漏,因為只有在使用了get和set,並且遍歷到了發生記憶體洩漏的下標才會清理,如果發生洩露後我們沒有呼叫相關方法去解決,那麼記憶體洩漏依然存在。
要完全解決記憶體洩漏有兩種方式:
- 結束執行緒,釋放掉ThreadLocalMap的記憶體:也就是打斷之前圖中的Thread到entry的強引用鏈,使垃圾回收清除entry。
- 手動呼叫remove方法:在使用完後使用remove移出entry