深入理解ThreadLocal及其變種

yejg1212發表於2022-03-04

ThreadLocal

定義

ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地執行緒”。

其實,ThreadLocal並不是一個Thread,而是Thread的區域性變數,也許把它命名為ThreadLocalVariable更容易讓人理解一些。

各個執行緒的ThreadLocal關聯的例項互不干擾。特徵:

  • ThreadLocal表示執行緒的"區域性變數",它確保每個執行緒的ThreadLocal變數都是各自獨立的
  • ThreadLocal適合在一個執行緒的處理流程中保持上下文(避免了同一引數在所有方法中傳遞)
  • 使用ThreadLocal要用try ... finally結構,並在finally中清除

常用方法

  • set:為當前執行緒設定變數,當前ThreadLocal作為索引
  • get:獲取當前執行緒變數,當前ThreadLocal作為索引
  • initialValue:(需要子類實現,預設mull)執行get時,發現執行緒本地變數為null,就會執行initialValue的內容
  • remove:清空當前執行緒的ThreadLocal索引與對映的元素

底層結構及邏輯

Thread物件的屬性

public class Thread implements Runnable {
    // .....
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // .....
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

ThreadLocalMap物件

public class ThreadLocal<T> {
	// .....
	
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }
    }    
}

set值流程

原始碼摘要:

// java.lang.ThreadLocal#set
public void set(T value) {
    Thread t = Thread.currentThread();
    // map惰性建立
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

重點來關注下 java.lang.ThreadLocal.ThreadLocalMap#set 方法

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 根據ThreadLocal物件的hash值,定位到table中的位置i
    int i = key.threadLocalHashCode & (len - 1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        
        // 如果位置i不為空,且這個Entry物件的key正好是即將設定的key,那麼就覆蓋Entry中的value
        if (k == key) {
            e.value = value;
            return;
        }
        
        // 如果當前位置是空的,就初始化一個Entry物件放在位置i上
        if (k == null) {
            // 裡面會調到 expungeStaleEntry 
            replaceStaleEntry(key, value, i);
            return;
        }
        
        // 如果位置i的不為空,而且key不等於entry,那就找下一個空位置,直到為空為止
    }
    
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

結合程式碼,set的過程如下圖

衝突解決

線性探測的方式解決hash衝突的問題,如果沒有找到空閒的slot,就不斷往後嘗試,直到找到一個空閒的位置,插入entry

get流程

原始碼摘要:

// java.lang.ThreadLocal#get
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();// 呼叫initialValue方法
}
// java.lang.ThreadLocal.ThreadLocalMap#getEntry
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        // 可能是沒有,或者hash衝突了
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // get的時候一樣是根據ThreadLocal獲取到table的i值,然後查詢資料拿到後會對比key是否相等
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 相等就直接返回,不相等就繼續查詢,找到相等位置。
        if (k == key)
            return e;
        if (k == null)
            // 清理回收無效value、entry
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

弱引用

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

為什麼使用弱引用?

弱引用的特點:弱引用的物件擁有更短暫的生命週期。垃圾回收器執行緒掃描的時候,一旦發現了具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。

結合到這裡的場景,當ThreadLocal在沒有外部強引用的時候,一旦發生gc,key就會被回收。

記憶體洩露問題

因為有了弱引用,可以確保Entry的key會被記憶體回收掉。但是Entry的value和Entry物件本身還是沒有得到回收。

如果ThreadLocal的執行緒一直保持執行,那麼這個Entry物件中的value就有可能一直得不到回收,發生記憶體洩露。

解決辦法:在finally裡面呼叫remove方法

擴充套件

InheritableThreadLocal

InheritableThreadLocal 是 JDK 本身自帶的一種執行緒傳遞解決方案,以完成父執行緒到子執行緒的值傳遞。在建立子執行緒的時候,就把父執行緒的ThreadLocal的內容複製過去。

// java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    // ...
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        // 複製父執行緒的InheritableThreadLocal內容
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    
    // ...
}


// java.lang.ThreadLocal#createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                // 這裡的value 是同一個物件
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

不過,子執行緒ThreadLocalMap裡的Entry.value指向的物件和父執行緒是同一個

特殊場景下的缺陷

線上程池的場景下,執行緒由執行緒池建立好,並且執行緒是池化起來反覆使用的;這時父子執行緒關係的ThreadLocal值傳遞已經沒有意義。比如:

public static void main(String[] args) throws InterruptedException {
    // 執行緒池提前建立好
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    // 提前建立了一個子執行緒 [pool-1-thread-1]
    executorService.submit(() -> {
        System.out.println(Thread.currentThread().getName());
    });
    Thread.sleep(1000);

    InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal();
    threadLocal.set("start");
    System.out.println(threadLocal.get());

    // 後續,[pool-1-thread-1]執行緒的ThreadLocal值永遠是null
    executorService.submit(() -> {
        System.out.println(threadLocal.get() + "   ->   " + Thread.currentThread().getName());
    });
    executorService.submit(() -> {
        System.out.println(threadLocal.get() + "   ->   " + Thread.currentThread().getName());
    });
    executorService.submit(() -> {
        System.out.println(threadLocal.get() + "   ->   " + Thread.currentThread().getName());
    });

    Thread.sleep(100);
    System.out.println(threadLocal.get());
    executorService.shutdown();
}

// 輸出結果
pool-1-thread-1
start
start   ->   pool-1-thread-2
start   ->   pool-1-thread-2
null   ->   pool-1-thread-1
start

尤其是現在都是基於框架開發,執行緒池一般在專案啟動的時候,就建立好了。業務程式碼提交執行任務的時候,如果複用之前的執行緒,那麼值就沒傳到子執行緒中去!

像這種情況,我們至少要求 把任務提交給執行緒池時 的ThreadLocal值傳遞到執行執行緒中。TransmittableThreadLocal的出現就是為了解決這個問題。

TransmittableThreadLocal

TransmittableThreadLocal是Alibaba開源的一個類,它繼承了InheritableThreadLocal。能實現線上程池和主執行緒之間傳遞,需要配合TtlRunnable 和 TtlCallable使用。

使用示例

public static void main(String[] args) throws InterruptedException {
    // 執行緒池提前建立好
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    // 提前建立了一個子執行緒 [pool-1-thread-1]
    executorService.submit(() -> {
        System.out.println(Thread.currentThread().getName());
    });
    Thread.sleep(1000);

    TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal();
    threadLocal.set("start");
    System.out.println(threadLocal.get());

    // 每次提交時都需要通過修飾操作(即TtlRunnable.get(task))以抓取這次提交時的TransmittableThreadLocal上下文的值
    executorService.submit(TtlRunnable.get(() -> {
        System.out.println(threadLocal.get() + "   ->   " + Thread.currentThread().getName());
    }));
    executorService.submit(TtlRunnable.get(() -> {
        System.out.println(threadLocal.get() + "   ->   " + Thread.currentThread().getName());
    }));
    executorService.submit(TtlRunnable.get(() -> {
        System.out.println(threadLocal.get() + "   ->   " + Thread.currentThread().getName());
    }));

    Thread.sleep(100);
    System.out.println(threadLocal.get());
    executorService.shutdown();
}

// 輸出結果
pool-1-thread-1
start
start   ->   pool-1-thread-1
start   ->   pool-1-thread-2
start   ->   pool-1-thread-1
start

整個過程的完整時序圖

修飾執行緒池

使用TTL的時候,每次提交任務時,都需要用TtlRunnable 或者 TtlCallable對任務修飾一下。這個修飾邏輯可以再執行緒池中完成。

通過工具類com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法:

  • getTtlExecutor:修飾介面Executor
  • getTtlExecutorService:修飾介面ExecutorService
  • getTtlScheduledExecutorService:修飾介面ScheduledExecutorService

示例程式碼:

ExecutorService executorService = ...
// 額外的處理,生成修飾了的物件executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

// 在父執行緒中設定
context.set("value-set-in-parent");

Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);

// =====================================================

// Task或是Call中可以讀取,值是"value-set-in-parent"
String value = context.get();

FastThreadLocal

前面分析了ThreadLocal的get和set,當遇到hash衝突的時候,會以nextIndex計算下一個位置的方式來解決hash衝突。

使用線性探測的方式解決hash衝突的問題,如果沒有找到空閒的slot,就不斷往後嘗試,直到找到一個空閒的位置,插入entry,這種方式在經常遇到hash衝突時,影響效率。

鑑於此,netty提供了FastThreadLocal。與之配套的還有FastThreadLocalThread和FastThreadLocalRunnable。

建立FastThreadLocal物件的時候,直接把位置index(使用AtomicInteger實現)確定下來。每個FastThreadLocal都能獲取到一個不重複的下標

public FastThreadLocal() {
    index = InternalThreadLocalMap.nextVariableIndex();
}


public static int nextVariableIndex() {
    int index = nextIndex.getAndIncrement();
    if (index < 0) {
        nextIndex.decrementAndGet();
        throw new IllegalStateException("too many thread-local indexed variables");
    }
    return index;
}

不過,FastThreadLocal需要配合FastThreadLocalThread使用,才能發揮它的效率。

public final void set(V value) {
    if (value != InternalThreadLocalMap.UNSET) {
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        setKnownNotUnset(threadLocalMap, value);
    } else {
        remove();
    }
}
public final V get() {
    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
    Object v = threadLocalMap.indexedVariable(index);// 直接定位
    if (v != InternalThreadLocalMap.UNSET) {
        return (V) v;
    }
    return initialize(threadLocalMap);
}
public Object indexedVariable(int index) {
    Object[] lookup = indexedVariables;
    return index < lookup.length? lookup[index] : UNSET;
}


// InternalThreadLocalMap.get()
public static InternalThreadLocalMap get() {
    Thread thread = Thread.currentThread();
    // 判斷當前Thread型別
    if (thread instanceof FastThreadLocalThread) {
        return fastGet((FastThreadLocalThread) thread);
    } else {
        return slowGet();
    }
}

private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
    // FastThreadLocalThread繼承Thread,額外有InternalThreadLocalMap型別屬性
    InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
    if (threadLocalMap == null) {
        thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
    }
    return threadLocalMap;
}

private static InternalThreadLocalMap slowGet() {
    // 普通Thread無InternalThreadLocalMap,但有ThreadLocal屬性,在它裡面存InternalThreadLocalMap等於間接有了InternalThreadLocalMap
    ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = UnpaddedInternalThreadLocalMap.slowThreadLocalMap;
    InternalThreadLocalMap ret = slowThreadLocalMap.get();
    if (ret == null) {
        ret = new InternalThreadLocalMap();
        slowThreadLocalMap.set(ret);
    }
    return ret;
}

也就是說,如果是普通Thread使用FastThreadLocal,則需要先拿到ThreadLocal物件,然後再get到裡面存的InternalThreadLocalMap。這一get過程完全是ThreadLocal的get,也需要執行hash碰撞&getEntryAfterMiss等邏輯。(有的地方稱之為退化

相關文章