前言
java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。如下圖所示:
其中堆是佔虛擬機器中記憶體最大的,堆被所有執行緒所共享,其最主要的便是存放例項物件。也因為堆記憶體是共享的,因此在多執行緒操作的條件下,多執行緒中堆記憶體中的資料十分容易發生執行緒安全的問題。因此為了保證多個執行緒對變數的安全訪問,我們可以將變數放到ThreadLocal物件中,變數在每個執行緒中都有獨立值,執行緒只能操作自己的變數,訪問不到其他執行緒中的變數。
ThreadLocal
ThreadLocal顧名思義便是執行緒本地變數的意思,在JAVA程式中每new一個ThreadLocal物件例項時,每個執行緒就會有一個隸屬於自己的變數,一個專屬於執行緒的變數,也因此該變數不會被其他的執行緒訪問到,以此來規避了執行緒安全的問題。
那麼ThreadLocal如何使得每個執行緒擁有自己獨有的本地值呢?
在JDK8的版本中,每一個執行緒都有一個屬於自己的ThreadLocalMap,ThreadLocalMap隨著Thread的建立而存在,隨著Thread的例項銷燬而銷燬。
//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即是儲存本地變數的關鍵之一,它首先以ThreadLocal例項和變數值作為Entry物件的構造引數來構造Entry物件,後以ThreadLocal例項進行雜湊計算hash,在雜湊函式計算後,每個ThreadLocal會均勻地、獨立地被分佈在Entry陣列中,也就是會得到自己在Entry陣列中的索引值,然後用此索引將構造出來的Entry物件放入到Entry陣列中。也由於每個執行緒都有自己的ThreadLocalMap,因此變數值是存放在專屬於自己執行緒的ThreadLocalMap中,這個ThreadLocalMap其他執行緒獲取不到,所以每個執行緒都有專屬於自己的變數值,在操作的時候也是對自己專屬的變數值進行操作。
從上圖我們也可以知道ThreadLocalMap其實是由ThreadLocal來進行管理的,兩者的關係密不可分。
我們在平時的操作中,大多是操作ThreadLocal的get(),set()方法,似乎ThreadLocalMap接觸的較少,但在接下來深入ThreadLocal的時候,我們會發現ThreadLocal這個類其實是基於ThreadLocalMap來完成的。
ThreadLocal的get
方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
上面的程式碼塊展示的是ThreadLocal中get方法的原始碼,通過原始碼我們可以瞭解到get方法有以下步驟:
- 首先它會獲取當前佔有CPU時間片的執行緒的例項,然後通過當前執行緒的例項呼叫getMap()方法來獲取當前執行緒的ThreadLocalMap。
//getMap()方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- 如果ThreadLocalMap不為空的話,就以自身ThreadLocal例項作為引數呼叫ThreadLocalMap的getEntry()方法來獲取到Entry物件,如果Entry物件不為空,則獲取Entry的value屬性值返回。
- 如果map為空的話或者此ThreadLocal例項計算出的hash值最為Entry陣列的索引在Entry陣列中並未存在Entry物件,證明當前執行緒並未初始化ThreadLocalMap,呼叫setInitialValue方法後返回。
ThreadLocal的setInitialValue
方法
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);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
我們來探尋get()方法中呼叫的setInitialValue
方法,在以下程式碼中我們可以知道:
- 首先一上來就會呼叫一個鉤子函式initialValue()來給value變數賦值,但是我們進入initialValue()方法卻發現這個方法的返回值是null,如果需要繼承ThreadLocal來重寫這個方法就太麻煩,JDK已經為大家定義了ThreadLocal的內部SuppliedThreadLocal靜態子類,並且提供了
ThreadLocal.withInitial()
靜態工廠方法,我們只需要在定義一個ThreadLocal型別變數時,使用這個方法。
initialValue鉤子函式只會呼叫一次,且只在不使用ThreadLocal.set()方法去設定值就使用ThreadLocal.get()方法去獲取值的時候,會執行。
//鉤子函式
protected T initialValue() {
return null;
}
//靜態工廠方法
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
- 與
get
方法的步驟一致,也是獲取當前的執行緒後,獲取當前執行緒的ThreadLocalMap,如果沒有的話則建立為ThreadLocalMap建立一個map,這是因為一開始Thread下面的ThreadLocalMap初始值為空,所以有create這個步驟。在creatMap的方法中,我們可以看到了新建了一個ThreadLocalMap類,並以當前的ThreadLocal例項物件和initialValue
產生的值作為構造引數,以此生成Entry物件儲存在Entry陣列中。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 最後方法返回。
ThreadLocal的set
方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
以上是ThreadLocal的set
方法的原始碼,它相對於get
方法較簡單,也是獲取當前執行緒並獲取當前執行緒的ThreadLocalMap,如果有的話呼叫ThreadLocalMap的set
方法將值設定進去,如果ThreadLocal為空,則使用createMap
方法去建立一個ThreadLocalMap。
ThreadLocal的remove
方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
ThreadLocal的remove
方法便更簡單了,它僅判斷獲取到的當前執行緒的ThreadLocalMap不為空,則呼叫了ThreadLocalMap的remove
方法去刪除值。
後續
從以上的ThreadLocal函式,我們可以看到,許多重要的方法都是依靠著ThreadLocalMap及其api去完成對ThreadLocal方法的實現的,不難看出理解ThreadLocalMap其實相較於理解ThreadLocal是比較重要的,而ThreadLocalMap內部對Entry這個子類的實現,更是考慮到了ThreadLocal的記憶體洩漏,因此使用了WeakReference
弱引用去關聯ThreadLocal例項,防止強引用導致的記憶體洩露的問題。
對ThreadLocalMap我會另開一個隨筆去寫,請多多擔待。
結尾
本文參考
[1] 周志明.深入理解Java虛擬機器:JVM高階特性與最佳實踐.-2版.北京:機械工業出版社,2013.6
[2] 尼恩.Java高併發程式設計.卷2,多執行緒、鎖、JMM、JUC、高併發設計模式.北京:機械工業出版社,2021,5