網上有很多關於ThreadLocal
的介紹,有的介紹比較簡單,也有的介紹很複雜,比較難懂,今天,自己結合它的原始碼,也做個簡易梳理,記錄如下!
ThreadLocal的作用
在多請求併發訪問過程中,我們往往需要將一個指定變數隔離起來,達到只對當前執行緒可用,其他執行緒不可用的效果,因此,我們就會使用到ThreadLocal來實現。
實現原理其實就是在每個執行緒中維護了一個Map結構(ThreadLocalMap,它是ThreadLocal中的靜態內部類),ThreadLocal物件為Key,需要隔離的值為Value。為了達到執行緒全域性可用,我們往往將ThreadLocal宣告為全域性靜態變數。
Thread中的ThreadLocalMap物件
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
那麼ThreadLocal具體如何做到執行緒隔離的?我們下面做具體分析!
ThreadLocal
我們暫時先不分析ThreadLocalMap,單獨來看ThreadLocal的幾個方法原始碼介紹!
1.物件初始化
ThreadLocal
初始化比較簡單!
public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
我們往往在初始化時會給他指定一個預設值,不指定的話,預設值為null
,這裡有兩種指定方式:
第一種:直接複寫ThreadLocal中的initialValue方法
第二種:利用函數語言程式設計,建立SuppliedThreadLocal物件,由get方法直接返回初始值
public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "Test";
}
};
public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(()->"Test");
SuppliedThreadLocal物件是對ThreadLocal的一個特定實現,通過建構函式傳入Supplier,再由實現的initialValue方法返回supplier.get()的結果,其他也沒什麼可多介紹的。
2.獲取變數
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();
}
從get方法我們可以看到,ThreadLocal是從當前執行緒中獲取到了ThreadLocalMap物件,然後取出其中的Entry.Value值,如果物件不存在就返回初始值,初始化方法initialValue會在這裡呼叫一次,其他操作不再呼叫。
3.設定變數
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方法與get方法一樣,會通過當前執行緒取出ThreadLocalMap物件,然後將當前ThreadLocal物件作為Key,儲存Value值,ThreadLocalMap不存在時,會建立新的Map。
4.移除變數
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
移除變數同樣是根據currentThread來找到的Map,然後對當前ThreadLocal做remove操作。
ThreadLocalMap
通過ThreadLocal的操作介紹我們可以看到,ThreadLocal的操作都是基於ThreadLocalMap來實現的,所以,ThreaLocalMap才是我們對ThreadLocal變數實現執行緒隔離的重點。
1.Entry
ThreadLocalMap中儲存資料關係的是Entry,它的Key是ThreadLocal物件,採用弱引用,Value是一個強引用物件Object。當Entry.get()獲取的ThreadLocal為Null時,GC回收將直接清除該物件,但Value物件,需要我們手動清除,所以,我們需要在每個ThreadLocal呼叫結束時,執行remove方法。
那麼,為什麼ThreadLocal中沒有使用普通的Key-Value形式定義儲存結構呢?
因為如果這裡使用普通的key-value形式來定義儲存結構,實質上就會造成節點的生命週期與執行緒強繫結,只要執行緒沒有銷燬,那麼節點在GC分析中一直處於可達狀態,沒辦法被回收,而程式本身也無法判斷是否可以清理節點。弱引用是Java中四檔引用的第三檔,比軟引用更加弱一些,如果一個物件沒有強引用鏈可達,那麼一般活不過下一次GC。當某個ThreadLocal已經沒有強引用可達,則隨著它被垃圾回收,在ThreadLocalMap裡對應的Entry的鍵值會失效,這為ThreadLocalMap本身的垃圾清理提供了便利。
關於弱引用與強引用的關係以及他們的物件回收機制,這裡不做過多介紹,有興趣的同學可以自行學習!
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
2.初始化
ThreadLocalMap的操作是基於Entry[]陣列table完成的,陣列初始化大小為16。table是一個2的N次方的陣列,ThreadLocal通過AtomicInteger型別的nextHashCode,每次偏移HASH_INCREMENT=0x61c88647
的大小來實現資料在陣列上的平均分佈。
Entry[]陣列table為什麼是一個2的N次方陣列呢?
第一個原因是ThreadLocalMap使用的是開放定址法中的線性探測法,均勻分佈的好處在於很快就能探測到下一個臨近的可用slot,從而保證效率。
第二個原因是位運算比取模效率高,rehash的時候只需要判斷0還是1。
關於Entry[]中如何解決碰撞衝突問題,可以參考:ThreadLocal 和神奇的數字 0x61c88647
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);
}
3.獲取Entry
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
查詢table中的Entry值時,採用神奇的0x61c88647
,ThreadLocal物件作為Key與Entry的Key相同時,返回此Entry,否則,採用開放定址法
,從i開始線性探測查詢Entry。
4.設定Entry
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;
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();
}
set方法中相容新增與修改操作,如果找到同一個ThreadLocal對應的Entry時,則直接重新賦值Value,否則新建Entry賦值給table[i]。
5.移除Entry
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;
}
}
}
remove操作同樣採用線性探測到指定的table[i],找到Key相同的ThreadLocal物件,然後通過指定弱引用的Key值為Null移除,並將table[i].value也置為Null,通過GC回收徹底刪除元素。
InheritableThreadLocal
ThreadLocal解決了執行緒隔離問題,但對於子執行緒想要獲取到父執行緒中的變數,又如何做呢?JDK為我們提供了另外一個執行緒本地變數實現類InheritableThreadLocal
。
InheritableThreadLocal繼承自ThreadLocal,與ThreadLocal一樣,它也會在Thread中定義一個Map結構來維護Entry訪問。
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
所以,每個Thread中都會儲存有當前執行緒的ThreadLocal物件threadLocals,和繼承父執行緒的ThreadLocal物件inheritableThreadLocals。
那麼,父執行緒資料如何傳遞給子執行緒的呢?我們來看Thread的init方法,是如何做的。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
執行緒初始化時,會將父執行緒的ThreadLocalMap傳給子執行緒,通過Entry[]陣列拷貝,完成子執行緒ThreadLocal物件的建立。具體操作在ThreadMap的另一構造方法完成。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
InheritableThreadLocal中重寫了ThreadLocal的三個方法:
childValue:獲取父執行緒變數值。
getMap:獲取繼承過來的ThreaLocal物件。
createMap:建立繼承父執行緒的ThreadLocal物件。
InheritableThreadLocal實現了Entry陣列拷貝後,其他操作方法與ThreadLocal相同。