ThreadLocal解析

zhong0316發表於2019-02-24

原理

產生執行緒安全問題的根源在於多執行緒之間的資料共享。如果沒有資料共享,就沒有多執行緒併發安全問題。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;
}
複製程式碼

設定初始值會呼叫initialValue()方法獲取初始值,該方法預設返回null,該方法可以被覆寫,用於設定初始值。例如上面的例子中,通過匿名內部類覆寫了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可以在子執行緒中訪問到父執行緒中的變數。

相關文章