ThreadLocal原始碼分析
概述
ThreadLocal提供了一種執行緒安全的資料訪問方式,每個執行緒中都存在一個共享變數副本,從而實現多執行緒狀態下的執行緒安全。
demo
public static void main(String[] args) { final ThreadLocal<Integer> MAIN = ThreadLocal.withInitial(() -> 100); MAIN.set(200); new Thread(()->{ System.out.println(Thread.currentThread().getName() + " MAIN:" + MAIN.get()); }).start(); System.out.println("MAIN:" + MAIN.get()); //一定要注意,當ThreadLocal不再使用時,一定要呼叫remove方法,以免記憶體洩漏 MAIN.remove(); System.out.println("MAIN:" + MAIN.get()); }
執行之後,列印結果如下:
MAIN:200
Thread-0 MAIN:100
MAIN是在主執行緒中set的值,可以在主執行緒中使用get方法獲得,但線上程中呼叫get方法,結果卻為null,這是為什麼呢?這個原因其實也是為什麼說ThreadLocal能被稱之為執行緒安全的原因。下面我們就通過原始碼來一探究竟。
關鍵屬性
//表示當前ThreadLocal的hashCode,用於計算當前ThreadLocal在ThreadLocalMap中的索引位置 private final int threadLocalHashCode = nextHashCode(); // static+ AtomInteger 保證了在一臺機器中每個ThreadLocal的threadLocalHashCode是唯一的 // 被static修飾十分關鍵,因為一個執行緒在處理業務時,ThreadLocalMap會被set多個ThreadLocal,多個 // ThreadLocal就依靠著threadLocalHashCode進行區分 private static AtomicInteger nextHashCode = new AtomicInteger(); // 增量常量 private static final int HASH_INCREMENT = 0x61c88647; //計算 ThreadLocal 的 hashCode 值(就是遞增) private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
常用方法
set方法
每個執行緒的set方法都是序列的,因而不會有執行緒安全的問題。
public void set(T value) { //獲取當前執行緒 Thread t = Thread.currentThread(); // 獲取ThreadLocalMap ThreadLocalMap map = getMap(t); // 當前的threadLocalMap之前有設定值,則直接進行設定,否則就初始化 if (map != null) map.set(this, value); else //初始化threadLocalMap createMap(t, value); } //獲取執行緒的threadLocalMap屬性 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } //初始化 ThreadLocalMap void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
get方法
get方法主要是從ThreadLocalMap中取出當前ThreadLocal儲存的值。
public T get() {
//獲取當前執行緒
Thread t = Thread.currentThread();
//獲取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//map不為null時
if (map != null) {
//從map中取出Entry,由於ThreadLocalMap在set時解決hash衝突的策略不同,get的邏輯也不同
ThreadLocalMap.Entry e = map.getEntry(this);
//entry不為空的話,讀取當前ThreadLocal中儲存的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;、
//返回值
return result;
}
}
//若是map為空 則將當前執行緒的ThreadLocal初始化 並返回初始值null
return setInitialValue();
}
private T setInitialValue() {
//獲取初始值
T value = initialValue();
// 獲取當前執行緒
Thread t = Thread.currentThread();
// 從當前執行緒中獲取ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果map不為null的話,直接進行set
if (map != null)
map.set(this, value);
else
//否則初始化ThreadLocalMap
createMap(t, value);
//返回值
return value;
}
//直接return null
protected T initialValue() {
return null;
}
remove方法
由於ThreadLocal在使用不當時可能存在記憶體洩漏的場景,因而,在使用完ThreadLocal使用完之後,一定要顯示的呼叫remove方法進行清除。
public void remove() {
//獲取當前執行緒繫結的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
// map不為null的話
if (m != null)
//從map中移除當前threadLocal對應的K-V
m.remove(this);
}
可以看出,無論是set、get還是remove 方法,其底層原理都比較簡單,但卻都包含一個共性,就是使用到了ThreadLocalMap,那麼ThreadLocalMap又是一個什麼東東呢?
ThreadLocalMap
ThreadLocalMap是ThreadLocal中的一個靜態內部類,其本質上是一個簡單的Map結構,key是ThreadLocal型別,value是ThreadLocal儲存的值,底層是一個Entry型別陣列組成的資料結構。Entry類的結構如下所示:
static class ThreadLocalMap {
// Entry繼承自WeakReference 因而Entry陣列中每個Entry節點也是一個弱引用,當沒有引用指向時,
//會被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
// 當前ThreadLocal關聯的值
Object value;
// WeakReference的引用 referent就是ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始容量大小
private static final int INITIAL_CAPACITY = 16;
// Entry陣列
private Entry[] table;
//Entry陣列大小
private int size = 0;
//閾值
private int threshold; // Default to 0
//設定閾值方法 可以看出是容量的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//計算索引的下一個位置
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
//計算索引的上一個位置
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
//構造方法
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);
}
.............
}
ThreadLocal實現執行緒隔離的原理
ThreadLocal是執行緒安全的,主要是因為ThreadLocalMap是執行緒Thread的一個屬性,如下所示:
threadLocals和inheritableThreadLocals分別是執行緒的兩個屬性,因而每個執行緒的ThreadLocals都是隔離獨享的。在Thread的init方法中,父執行緒在建立子執行緒的情況下,會拷貝inheritableThreadLocals的值,但不會拷貝threadLocals的值,如下所示:
// Thread中的init方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
...
//當父執行緒的inheritableThreadLocals的值不為空時
// 會把inheritable裡面的值全部傳遞給子執行緒
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
//
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
//ThreadLocal中的createInheritedMap方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
線上程建立時,會把父執行緒的inheritableThreadLocals屬性值進行拷貝。
set方法
private void set(ThreadLocal<?> key, Object value) {
//儲存entry陣列
Entry[] tab = table;
// 獲取陣列長度
int len = tab.length;
// 計算索引下標位置
int i = key.threadLocalHashCode & (len-1);
// 整體策略:檢視 i 索引位置有沒有值,有值的話,索引位置 + 1,直到找到沒有值的位置
// 這種解決 hash 衝突的策略,也導致了其在 get 時查詢策略有所不同,體現在 getEntryAfterMiss 中
for (Entry e = tab[i];
e != null;
// nextIndex 就是讓在不超過陣列長度的基礎上,把陣列的索引位置 + 1
e = tab[i = nextIndex(i, len)]) {
//獲取threadLocal
ThreadLocal<?> k = e.get();
//如果二者相等 直接替換並返回
if (k == key) {
e.value = value;
return;
}
//如果為空 說明當前的threadLocal被清理了,直接替換並返回
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//當前i位置沒有值的話,直接生成一個Entry
tab[i] = new Entry(key, value);
// 維護size
int sz = ++size;
// 當陣列大小大於等於擴容閾值(陣列大小的三分之二)時,進行擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
需要注意幾點的是:
- 通過遞增的AtomicInteger作為ThreadLocal的hashCode的;
- 通過計算hashCode計算的索引位置i處,如果已經有值的話,會從i開始,通過+1,不斷往後尋找,直到找到索引位置為空的地方,把當前ThreadLocal作為key放進去;
getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
// 計算索引值
int i = key.threadLocalHashCode & (table.length - 1);
// 獲取索引處的entry
Entry e = table[i];
// e 不為空,並且 e 的 ThreadLocal 的記憶體地址和 key 相同,直接返回,否則就是沒有找到,
//繼續通過 getEntryAfterMiss 方法找
if (e != null && e.get() == key)
return e;
else
// 這個取資料的邏輯,是因為 set 時陣列索引位置衝突造成的
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
//暫存entry陣列
Entry[] tab = table;
// 獲取陣列長度
int len = tab.length;
//遍歷陣列
while (e != null) {
// 記憶體地址一樣,表示找到了 直接返回即可
ThreadLocal<?> k = e.get();
if (k == key)
return e;
//如果為null的話 刪除沒用的key
if (k == null)
expungeStaleEntry(i);
else
//否計算出下一個索引的位置
i = nextIndex(i, len);
//繼續下一次遍歷
e = tab[i];
}
//如果最後entry陣列遍歷結束都沒有找到,直接返回null
return null;
}
resize方法
//擴容
private void resize() {
//暫存舊的entry陣列
Entry[] oldTab = table;
//舊陣列的容量
int oldLen = oldTab.length;
//新陣列的容量
int newLen = oldLen * 2;
//初始化一個空的新陣列
Entry[] newTab = new Entry[newLen];
//記錄數量
int count = 0;
//開始複製
for (int j = 0; j < oldLen; ++j) {
//舊陣列的一個節點
Entry e = oldTab[j];
//不為null的話開始進行復制
if (e != null) {
//獲取threadLocal
ThreadLocal<?> k = e.get();
//為null的話直接進行清除
if (k == null) {
e.value = null; // Help the GC
} else {
//計算threadLocal在新entry中的索引位置
int h = k.threadLocalHashCode & (newLen - 1);
//如果該位置以及有值了,那麼就尋找下一個索引位置,直到為空
while (newTab[h] != null)
h = nextIndex(h, newLen);
//將值拷貝到新陣列的位置
newTab[h] = e;
//更新數量
count++;
}
}
}
//重新設定擴容時的閾值,新陣列長度的2/3
setThreshold(newLen);
//維護size大小
size = count;
//將entry陣列的引用指向新陣列
table = newTab;
}
擴容時的邏輯也比較清晰:
- 擴容時新陣列大小為原來的兩倍;
- 擴容時沒有執行緒安全問題,因為ThreadLocalMap是執行緒本身的一個屬性,一個執行緒同一時刻只能對ThreadLocalMap進行操作,因為同一個執行緒執行業務邏輯時必然是序列的,那麼操作ThreadLocalMap必然也是序列的;
ThreadLocal記憶體洩漏原因探究
在demo演示中我們提到過,在使用ThreadLocal的過程中,如果使用不當,最後可能會導致記憶體洩漏的問題,那麼原因是什麼呢?
先來明確一下記憶體洩漏的概念:
記憶體洩漏主要有兩種情況,一是堆中申請的空間沒有被釋放;二是物件已不再被使用,但仍在記憶體中保留著。
先來看一下ThreadLocal和當前Thread在堆疊中的佈局圖吧。
注:上面的連線線中,實線代表強引用,虛線代表弱引用。
上面我們已經說過了,ThreadLocalMap中的Entry靜態類繼承了WeakReference類,它的Key是ThreadLocal物件的弱引用。那什麼是弱引用呢?根據《深入理解Java虛擬機器 第二版》中的定義:
弱引用是用來描述非必需物件的,被弱引用關聯的物件只能生存到下一次垃圾回收發生之前。當垃圾回收器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。
一般情況下,當我們不再使用threadLocal變數時,會手動將該變數置為null,這樣堆中的threadLocal例項物件將不會再被任何強引用所指向,這樣垃圾回收器就可以對其進行回收。此時,根據上圖我們可知,在垃圾回收之後,ThreadLocalMap中的Entry的key已經變成了null。但是,如果此時執行緒還存活著繼續執行,則key為null,但value指向Object(存在著強引用關係)的Entry物件仍然不會被回收,此時就會發生記憶體洩漏。當然,如果執行緒在完成任務之後就結束了生命週期,那麼隨後ThreadLocalMap和Entry也會隨之消亡。但如果使用的是執行緒池,執行緒在完成任務之後會回放到執行緒池中從而繼續被複用,那麼此時value就會一直存在,導致記憶體洩漏。
那麼問題來了,既然弱引用可能導致記憶體洩漏,那麼改為強引用呢?
還是跟上面一樣,一起來分析一下。當手動將threadLocal置為null時,雖然threadLocal Ref—ThreadLocal例項之間沒有引用關係,但Entry中key與ThreadLocal之間仍然存在著強引用關係,就會產生key和value都不為null的Entry物件,但是ThreadLocal我們明明已經不需要了,且只有執行緒一直執行下去,那麼threadLocal例項還是無法被回收,這樣還是會發生記憶體洩漏。
因而,雖然弱引用同樣也會導致記憶體洩漏的問題,但ThreadLocal的set、get以及remove操作都會清除ThreadLocalMap中Entry陣列中key為null的Entry,從而降低出現記憶體洩漏的風險。