[Java併發]ThreadLocal補充

Duancf發表於2024-08-09

ThreadLocal缺點及解決方案

每個Thread上都有一個threadLocals屬性,它是一個ThreadLocalMap,裡面存放著一個Entry陣列,key是ThreadLocal型別的弱引用,value是對用的值。所有的操作都是基於這個ThreadLocalMap操作的。

但是它有一個侷限性,就是不能在父子執行緒之間傳遞。 即在子執行緒中無法訪問在父執行緒中設定的本地執行緒變數。 後來為了解決這個問題,引入了一個新的類InheritableThreadLocal。

使用該方法後,子執行緒可以訪問在建立子執行緒時父執行緒當時的本地執行緒變數,其實現原理就是在父執行緒建立子執行緒時將父執行緒當前存在的本地執行緒變數複製到子執行緒的本地執行緒變數中。

public class InheritableThreadLocal extends ThreadLocal {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
從上面的結構中可以看出,它主要是重寫了getMap、createMap方法。

Thread類中有兩個重要的變數

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

init()方法片段

Thread parent = currentThread();
.....
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
子執行緒時透過在父執行緒透過呼叫new Thread()方法來建立子執行緒,Thread#init方法在Thread的構造方法中被呼叫。

主要是先獲取當前執行緒物件,即待建立的執行緒的父執行緒
如果父執行緒的inheritableThreadLocals不為空,並且inheritThreadLocals為true(預設為true),則使用父執行緒的ingerit本地變數的值來建立子執行緒的inheritableThreadLocals結構,即將父執行緒中的本地變數複製到子執行緒中。

private Entry[] table;

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) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {
                    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++;
                }
            }
        }
    }

子執行緒預設複製父執行緒的方式是淺複製,如果需要使用深複製,如果需要使用深複製,需要使用自定義ThreadLocal,繼承InheritThreadLocal並重寫childValue方法。

而它的實現原理主要是在建立子執行緒時將父類執行緒中的本地變數值parent.inheritableThreadLocals複製到子執行緒,即複製的機會在建立子執行緒時。

但是它也有一種缺陷,就是在使用執行緒池的情況下,因為執行緒池是複用執行緒,不會重複建立,而上述的inheritableThreadLocals是在建立子執行緒時才會將父執行緒的值複製到子執行緒,但是線上程池中不會重複建立,所以多次使用後,仍然記錄的是第一次提交任務時的外部執行緒的值,造成了資料的錯誤。

那如何解決這種現象呢?

只需在使用者執行緒向執行緒池提交任務時複製父執行緒的上下文環境,就可以實現本地變數線上程池呼叫的透傳。 基於這個思想,阿里提出了TransmittableThreadLocal類。

image

在提交任務時的Runable或者Callable必須封裝成TtlRunable或者TtlCallable。TransmittableThreadLocal覆蓋實現了ThreadLocal的set、get、remove,實際儲存inheritableThreadLocal值的工作還是inheritableThreadLocal父類完成,TransmittableThreadLocal只是為每個使用它的Thread單獨記錄一份儲存了哪些TransmittableThreadLocal物件。
public final void set(T value) {
super.set(value);
if (null == value) removeValue();
else addValue();
}
首先它會呼叫父類的InheritableThreadLocal的set方法,將value加入到Thread物件的inheritableThreadLocals變數中。
如果value為null,則呼叫removeValue()方法,否則呼叫addValue方法。

private void addValue() {
if (!holder.get().containsKey(this)) { // @1
holder.get().put(this, null); // WeakHashMap supports null value.
}
}
private void removeValue() {
holder.get().remove(this);
}
呼叫addValue方法,會將當前ThreadLocal儲存到TransmittableThreadLocal的全域性靜態變數hodler。所有和Thread繫結的所有TransmittableThreadLocal物件都儲存在這個holder中,holder只是為了記錄當前Thread繫結了哪些TransmittableThreadLocal物件。

下面具體講一下TtlRunable的原理。

private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
this.capturedRef = new AtomicReference(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
先建立Map容器,用來儲存父執行緒的本地執行緒變數,鍵為在父執行緒執行過程中的TransmittableLocal執行緒;將執行緒中的值存放在裡面。預設是淺複製,需要深複製的話,要重寫copyValue方法。

public void run() {
Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
Object backup = replay(captured);
try {
runnable.run();
} finally {
restore(backup);
}
}
TtlRunable是實現於Runable,所以執行緒池執行的是TtlRunable,但是在TtlRunnable run方法中國會執行Runable run方法。

TtlRunable構造方法中,呼叫了capture()獲取當前執行緒中所有的上下文,並儲存在AtomicReference中。

當執行緒執行時,呼叫TtlRunable run方法, TtlRunable會從AtomicReference中獲取出呼叫執行緒中的上下文,並把上下文利用replay方法把上下文複製到當前執行緒,並把上下文備份。

當執行緒執行完,呼叫restore把備份的上下文傳入,恢復備份的上下文傳入,把後面新增的上下文刪除,並重新把上下文複製到當前執行緒。本次執行並不會汙染執行緒池中執行緒原先的上下文環境。

相關文章