前言
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