ThreadLocal解析

原理

產生線程安全問題的根源在于多線程之間的數據共享。如果沒有數據共享,就沒有多線程并發安全問題。ThreadLocal就是用來避免多線程數據共享從而避免多線程并發安全問題。它為每個線程保留一個對象的副本,避免了多線程數據共享。每個線程作用的對象都是線程私有的一個對象拷貝。一個線程的對象副本無法被其他線程訪問到(InheritableThreadLocal除外)。
注意ThreadLocal并不是一種多線程并發安全問題的解決方案,因為ThreadLocal的原理在于避免多線程數據共享從而實現線程安全。
來看一下JDK文檔中ThreadLocal的描述:

This class provides thread-local variables.  These variables differ from
their normal counterparts in that each thread that accesses one (via its
{@code get} or {@code set} method) has its own, independently initialized
copy of the variable.  {@code ThreadLocal} instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).

Each thread holds an implicit reference to its copy of a thread-local
variable as long as the thread is alive and the {@code ThreadLocal}
instance is accessible; after a thread goes away, all of its copies of
thread-local instances are subject to garbage collection (unless other
references to these copies exist).

其大致的意思是:ThreadLocal為每個線程保留一個對象的副本,通過set()方法和get()方法來設置和獲取對象。ThreadLocal通常被定義為私有的和靜態的,用于關聯線程的某些狀態。當關聯ThreadLocal的線程死亡后,ThreadLocal實例才可以被GC。也就是說如果ThreadLocal關聯的線程如果沒有死亡,則ThreadLocal就一直不能被回收。

用法

創建

ThreadLocal<String> mThreadLocal = new ThreadLocal<>();

set方法

mThreadLocal.set(Thread.currentThread().getName());

get方法

String mThreadLocalVal = mThreadLocal.get();

設置初始值

ThreadLocal<String> mThreadLocal = ThreadLocal.withInitial(() -> Thread.currentThread().getName());

完整示例

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadId {

    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);
    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
            ThreadLocal.withInitial(() -> nextId.getAndIncrement());

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }
}

源碼分析

ThreadLocal為每個線程維護一個哈希表,用于保存線程本地變量,哈希表的key是ThreadLocal實例,value就是需要保存的對象。

publlic class Thread {
    ...
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

public class ThreadLocal {
    ...
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
        ...
    ...
}

threadLocals是非靜態的,也就是說每個線程都會有一個ThreadLocalMap哈希表用來保存本地變量。ThreadLocalMap的Entry的鍵(ThreadLocal<?>)是弱引用的,也就是說當垃圾收集器發現這個弱引用的鍵時不管內存是否足夠多將其回收。這里回收的是ThreadLocalMap的Entry的ThreadLocal而不是Entry,因此還是可能會造成內存泄露。

get()方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 獲取線程關聯的ThreadLocalMap哈希表
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); // 獲取entry
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value; // 返回entry關聯的對象
            return result;
        }
    }
    return setInitialValue(); // 如果當前線程關聯的本地變量哈希表為空,則創建一個
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

首先通過getMap()方法獲取當前線程的ThreadLocalMap實例,ThreadLocalMap是線程私有的,因此這里是線程安全的。ThreadLocalMap的getEntry()方法用于獲取當前線程關聯的ThreadLocalMap鍵值對,如果鍵值對不為空則返回值。如果鍵值對為空,則通過setInitialValue()方法設置初始值,并返回。注意setInitialValue()方法是private,是不可以覆寫的。

設置初始值

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;
}

<font color="#FF0000">設置初始值會調用initialValue()方法獲取初始值,該方法默認返回null,該方法可以被覆寫,用于設置初始值。</font>例如上面的例子中,通過匿名內部類覆寫了initialValue()方法設置了初始值。獲取到初始值后,判斷當前線程關聯的本地變量哈希表是否為空,如果非空則設置初始值,否則先新建本地變量哈希表再設置初始值。最后返回這個初始值。

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);
}

該方法先獲取該線程的 ThreadLocalMap 對象,然后直接將 ThreadLocal 對象(即代碼中的 this)與目標實例的映射添加進 ThreadLocalMap 中。當然,如果映射已經存在,就直接覆蓋。另外,如果獲取到的 ThreadLocalMap 為 null,則先創建該 ThreadLocalMap 對象。

防止內存泄露

前面分析得知ThreadLocalMap的Entry的key是弱引用的,key可以在垃圾收集器工作的時候就被回收掉,但是存在 當前線程->ThreadLocal->ThreadLocalMap->Entry的一條強引用鏈,因此如果當前線程沒有死亡,或者還持有ThreadLocal實例的引用Entry就無法被回收。從而造成內存泄露。
當我們使用線程池來處理請求的時候,一個請求處理完成,線程并不一定會被回收,因此線程還會持有ThreadLocal實例的引用,即使ThreadLocal已經沒有作用了。此時就發生了ThreadLocal的內存泄露。
針對該問題,ThreadLocalMap 的 set 方法中,通過 replaceStaleEntry 方法將所有鍵為 null 的 Entry 的值設置為 null,從而使得該值可被回收。另外,會在 rehash 方法中通過 expungeStaleEntry 方法將鍵和值為 null 的 Entry 設置為 null 從而使得該 Entry 可被回收。通過這種方式,ThreadLocal 可防止內存泄漏。

private void set(ThreadLocal<?> key, Object value) {
    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); // key為空,則代表該Entry不再需要,設置Entry的value指針和Entry指針為null,幫助GC
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

使用場景

ThreadLocal場景的使用場景:

  • 每個線程需要有自己單獨的實例
  • 實例需要在多個方法中共享,但不希望被多線程共享
    例如用來解決數據庫連接、Session管理等。

InheritableThreadLocal

ThreadLocal為每個線程保留一個線程私有的對象副本,線程之間無法共享訪問,但是有一個例外:InheritableThreadLocal,InheritableThreadLocal可以實現在子線程中訪問父線程中的對象副本。下面是一個InheritableThreadLocal和ThreadLocal區別的例子:

public class InheritableThreadLocalExample {

    public static void main(String[] args) throws InterruptedException {
        InheritableThreadLocal<Integer> integerInheritableThreadLocal = new InheritableThreadLocal<>();
        ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>();
        integerInheritableThreadLocal.set(1);
        integerThreadLocal.set(0);
        Thread thread = new Thread(() -> System.out.println(Thread.currentThread().getName() + ", " + integerThreadLocal.get() + " / " + integerInheritableThreadLocal.get()));
        thread.start();
        thread.join();
    }
}

運行上述代碼會發現在子線程中可以獲取父線程的InheritableThreadLocal中的變量,但是無法獲取父線程的ThreadLocal中的變量。
InheritableThreadLocal是ThreadLocal的子類:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    ...
    // 覆寫了ThreadLocal的getMap方法,返回的是Thread中的inheritableThreadLocals
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    // 覆寫了createMap方法,創建的也是Thread中的inheritableThreadLocals這個哈希表
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

而Thread的inheritableThreadLocals會在線程初始化的時候進行初始化。這個過程在Thread類的init()方法中:

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc) {
    ...
    Thread parent = currentThread();
    ...
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ...
}

一個線程在初始化的時候,會判斷創建這個線程的父線程的inheritableThreadLocals是否為空,如果不為空,則會拷貝父線程inheritableThreadLocals到當前創建的子線程的inheritableThreadLocals中去。
當我們在子線程調用get()方法時,InheritableThreadLocal的getMap()方法返回的是Thread中的inheritableThreadLocals,而子線程的inheritableThreadLocals已經拷貝了父線程的inheritableThreadLocals,因此在子線程中可以讀取父線程中的inheritableThreadLocals中保存的對象。

總結

  • ThreadLocal為每個線程保留對象副本,多線程之間沒有數據共享。因此它并不解決線程間共享數據的問題。
  • 每個線程持有一個Map并維護了ThreadLocal對象與具體實例的映射,該Map由于只被持有它的線程訪問,故不存在線程安全以及鎖的問題。
  • ThreadLocalMap的Entry對ThreadLocal的引用為弱引用,避免了ThreadLocal對象無法被回收的問題。
  • ThreadLocalMap的set方法通過調用 replaceStaleEntry 方法回收鍵為 null的Entry 對象的值(即為具體實例)以及Entry對象本身從而防止內存泄漏。
  • ThreadLocal 適用于變量在線程間隔離且在方法間共享的場景。
  • ThreadLocal中的變量是線程私有的,其他線程無法訪問到另外一個線程的變量。但是InheritableThreadLocal是個例外,通過InheritableThreadLocal可以在子線程中訪問到父線程中的變量。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容