在上家公司做spark的任務調度系統時,碰到過這么一個需求:
1.任務由一個線程執行,同時在執行過程中會創建多個線程執行子任務,子線程在執行子任務時又會創建子線程執行子任務的子任務。整個任務結構就像一棵高度為3的樹。
2.每個任務在執行過程中會生成一個任務ID,我需要把這個任務ID傳給子線程執行的子任務,子任務同時也會生成自己的任務ID,並把自己的任務ID向自己的子任務傳遞。
流程可由下圖所示
解決方案有很多,比如借助外部存儲如數據庫,或者自己在內存中維護一個存儲ID的數據結構。考慮到系統健壯性和可維護性,最后采用了jdk中的InheritableThreadLocal來實現這個需求。
來看下InheritableThreadLocal的結構
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
InheritableThreadLocal繼承自ThreadLocal,ThreadLocal可以說是一個存儲線程私有變量的容器(當然這個說法嚴格來說不准確,后面我們就知道為什么),而InheritableThreadLocal正如Inheritable所暗示的那樣,它是可繼承的:使用它可使子線程繼承父線程的所有線程私有變量。因此我寫了個工具類,底層使用InheritableThreadLocal來存儲任務的ID,並且使該ID能夠被子線程繼承。
public class InheritableThreadLocalUtils {
private static final ThreadLocal<Integer> local = new InheritableThreadLocal<>();
public static void set(Integer t) {
local.set(t);
}
public static Integer get() {
return local.get();
}
public static void remove() {
local.remove();
}
}
可以通過這個工具類的set方法和get方法分別實現任務ID的存取。然而在Code Review的時候,有同事覺得我這代碼寫的有問題:原因大概是InheritableThreadLocal在這里只有一個,子線程的任務ID在存儲的時候會相互覆蓋掉。真的會這樣嗎?為此我們用代碼測試下:
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
executorService.execute(new TaskThread(i));
}
}
static class TaskThread implements Runnable{
Integer taskId;
public TaskThread(Integer taskId) {
this.taskId = taskId;
}
@Override
public void run() {
InheritableThreadLocalUtils.set(taskId);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(InheritableThreadLocalUtils.get());
}
});
}
}
這段代碼開啟了10個線程標號從0到9,我們在每個線程中將對應的標號存儲到InheritableThreadLocal,然后開啟一個子線程,在子線程中獲取InheritableThreadLocal中的變量。最后的結果如下

每個線程都准確的獲取到了父線程對應的ID,可見並沒有覆蓋的問題。InheritableThreadLocal確實是用來存儲和獲取線程私有變量的,但是真實的變量並不是存儲在這個InheritableThreadLocal對象中,它只是為我們存取線程私有變量提供了入口而已。因為InheritableThreadLocal只是在ThreadLocal的基礎上提供了繼承功能,為了弄清這個問題我們研究下ThreadLocal的源碼。
2. ThreadLocal源碼解析
ThreadLocal主要方法有兩個,一個set用來存儲線程私有變量,一個get用來獲取線程私有變量。
2.1 set方法源碼解析
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
Thread t = Thread.currentThread()獲取了當前線程實例t,繼續跟進第二行的getMap方法,
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
t是線程實例,而threadLocals明顯是t的一個成員變量,進入一探究竟
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是個什么結構?
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap是類Thread中的一個靜態內部類,看起來像一個HashMap,但和HashMap又有些不一樣(關於它們的區別后面會講),那我們就把它當一個特殊的HashMap好了。因此set方法中第二行代碼
ThreadLocalMap map = getMap(t)是通過線程實例t得到一個ThreadLocalMap。接下來的代碼
if (map != null)
map.set(this, value);
else
createMap(t, value);
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
如果這個threadlocalmap為null,先創建一個threadlocalmap,然后以當前threadlocal對象為key,以要存儲的變量為值存儲到threadlocalmap中。
2.2 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();
}
首先獲取當前線程實例t,然后通過getMap(t)方法得到threadlocalmap(ThreadLocalMap是Thread的成員變量)。若這個map不為null,則以threadlocal為key獲取線程私有變量,否則執行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);
return value;
}
protected T initialValue() {
return null;
}
首先獲取threadlocal的初始化值,默認為null,可以通過重寫自定義該值;如果threadlocalmap為null,先創建一個;以當前threadlocal對象為key,以初始化值為value存入map中,最后返回這個初始化值。
2.3 ThreadLocal源碼總結
總的來說,ThreadLocal的源碼並不復雜,但是邏輯很繞。現總結如下:
- 1.ThreadLocal對象為每個線程存取私有的本地變量提供了入口,變量實際存儲在線程實例的內部一個叫ThreadLocalMap的數據結構中。
- 2.ThreadLocalMap是一個類HashMap的數據結構,Key為ThreadLoca對象(其實是一個弱引用),Value為要存儲的變量值。
- 3.使用ThreadLocal進行存取,其實就是以ThreadLocal對象為隱含的key對各個線程私有的Map進行存取。
可以用下圖的內存圖像幫助理解和記憶

3. ThreadLocalMap詳解
先看源碼
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
3.1 ThreadLocalMap的key為弱引用
ThreadLocalMap的key並不是ThreadLocal,而是WeakReference<ThreadLocal>,這是一個弱引用,說它弱是因為如果一個對象只被弱引用引用到,那么下次垃圾收集時就會被回收掉。如果引用ThreadLocal對象的只有ThreadLocalMap的key,那么下次垃圾收集過后該key就會變為null。
3.2 為何要用弱引用
減少了內存泄漏。試想我曾今存儲了一個ThreadLocal對象到ThreadLocalMap中,但后來我不需要這個對象了,只有ThreadLocalMap中的key還引用了該對象。如果這是個強引用的話,該對象將一直無法回收。因為我已經失去了其他所有該對象的外部引用,這個ThreadLocal對象將一直存在,而我卻無法訪問也無法回收它,導致內存泄漏。又因為ThreadLocalMap的生命周期和線程實例的生命周期一致,只要該線程一直不退出,比如線程池中的線程,那么這種內存泄漏問題將會不斷積累,直到導致系統奔潰。而如果是弱引用的話,當ThreadLocal失去了所有外部強引用的話,下次垃圾收集該ThreadLocal對象將被回收,對應的ThreadLocalMap中的key將為null。下次get和set方法被執行時將會對key為null的Entry進行清理。有效的減少了內存泄漏的可能和影響。
3.3 如何真正避免內存泄漏
- 及時調用ThreadLocal的remove方法
- 及時銷毀線程實例
4. 總結
ThreadLocal為我們存取線程私有變量提供了入口,變量實際存儲在線程實例的map結構中;使用它可以讓每個線程擁有一份共享變量的拷貝,以非同步的方式解決多線程對資源的爭用
