ThreadLocal線程間傳遞數(shù)據(jù)

局限性

局限性一:父線程無法通過ThreadLocal向子線程傳遞線程私有數(shù)據(jù)

ThreadLocal本意上就是線程私有的數(shù)據(jù)(從命名上也可以看出來),每個(gè)線程維護(hù)著自己的一份副本,線程與線程之間數(shù)據(jù)隔離。父線程若要向子線程傳遞線程私有數(shù)據(jù),肯定是不能通過ThreadLocal自己來實(shí)現(xiàn)耍属,否則也就不能叫ThreadLocal

如果使用異步任務(wù)時(shí),就涉及到線程間的數(shù)據(jù)傳遞,此時(shí)ThreadLocal就無法滿足條件

ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("foo");

Thread b = new Thread(() -> {
    String str = tl.get();
    // 企圖拿到父線程放進(jìn)去的foo恤煞,然而失敗了!
    System.out.println(str);
});

b.start();

// 子線程B啟動(dòng)后施籍,父線程A休息3秒居扒,確保子線程執(zhí)行完畢
TimeUnit.SECONDS.sleep(3);

// 清理
tl.remove();
解決方案:使用InheritableThreadLocal

InheritableThreadLocal是ThreadLocal的子類,它就是用來解決父線程向子線程傳遞線程私有數(shù)據(jù)問題的法梯。示例代碼只改第一行:ThreadLocal<String> tl = new ThreadLocal<>();改為ThreadLocal<String> tl = new InheritableThreadLocal<>();苔货,其它不變,如下示:

ThreadLocal<String> tl = new InheritableThreadLocal<>();
tl.set("foo");

Thread b = new Thread(() -> {
    String str = tl.get();
    // 企圖拿到父線程放進(jìn)去的foo立哑,成功了夜惭!因?yàn)閠l是InheritableThreadLocal
    System.out.println(str);
});

b.start();

// 子線程B啟動(dòng)后,父線程A休息3秒铛绰,確保子線程執(zhí)行完畢
TimeUnit.SECONDS.sleep(3);

// 清理
tl.remove();

可以看到诈茧,使用InheritableThreadLocal后,子線程能夠通過tl獲取父線程中set的值捂掰,實(shí)現(xiàn)了父線程向子線程傳遞線程私有數(shù)據(jù)的能力

這背后的原理是什么呢?

首先敢会,Thread類有兩個(gè)ThreadLocalMap類型的成員變量:threadLocals、inheritableThreadLocals这嚣,ThreadLocal使用的是threadLocals鸥昏,而InheritableThreadLocal使用的是inheritableThreadLocals

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

/*
 * InheritableThreadLocal values pertaining to this thread. This map is
 * maintained by the InheritableThreadLocal class.
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

執(zhí)行tl.set("foo");時(shí),對應(yīng)的邏輯如下:

// java.lang.ThreadLocal#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);
}

由于是第一次執(zhí)行姐帚,map為空吏垮,走createMap的邏輯,而不同的tl實(shí)現(xiàn)類(ThreadLocal罐旗、InheritableThreadLocal)有著不同的創(chuàng)建map邏輯


image.png

ThreadLocal的如下:

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

InheritableThreadLocal的如下:

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

很輕易能夠看出區(qū)別:ThreadLocal是給當(dāng)前線程的threadLocals變量賦值膳汪,而InheritableThreadLocal則是給當(dāng)前線程的inheritableThreadLocals變量賦值
其次,在創(chuàng)建線程實(shí)例九秀,即new Thread()時(shí)遗嗽,父線程使用自己的inheritableThreadLocals通過調(diào)用createInheritedMap方法來構(gòu)造子線程的ThreadLocalMap并賦值給子線程的inheritableThreadLocals

// java.lang.Thread#init

Thread parent = currentThread();

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
    // 父線程的Entry
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    // 依次"拷貝"到子線程的Entry里
    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++;
            }
        }
    }
}

注意上邊標(biāo)注的一行:Object value = key.childValue(e.value);,方法childValue是一個(gè)可重載方法鼓蜒,對于InheritableThreadLocal而言痹换,直接返回了傳進(jìn)來的值:

protected T childValue(T parentValue) {
    return parentValue;
}

這意味著是:淺拷貝征字,即默認(rèn)情況下,父線程傳遞給子線程的線程私有數(shù)據(jù)是一份淺拷貝晴音,若拷貝指向個(gè)不可變對象柔纵,這不是什么問題;若指向一個(gè)引用锤躁,那尤其需要注意搁料,因?yàn)椴辉儆芯€程封閉,父子線程中任意的變更均會(huì)影響到對方系羞。我們說它是可重載方法郭计,意味著若要實(shí)現(xiàn)"Deep Copy",需要自行繼承InheritableThreadLocal類并重寫childValue方法

小結(jié)一下解決方案背后的原理:使用InheritableThreadLocal椒振,會(huì)將線程私有數(shù)據(jù)存儲(chǔ)在inheritableThreadLocals指向的ThreadLocalMap中昭伸;在構(gòu)造子線程時(shí),將當(dāng)前線程inheritableThreadLocals里的數(shù)據(jù)(ThreadLocalMap)"拷貝"給子線程的ThreadLocalMap澎迎,子線程因此可以通過tl.get()取到數(shù)據(jù)庐杨,如此便實(shí)現(xiàn)了父線程向子線程傳遞線程私有數(shù)據(jù)

局限性二:父線程無法通過InheritableThreadLocal向池化的子線程(線程池)傳遞線程私有數(shù)據(jù)

在日常開發(fā)過程中,由于構(gòu)造與銷毀子線程開銷大夹供,因此每次在業(yè)務(wù)代碼中重新構(gòu)造一個(gè)子線程的方式并不常見灵份,更常見的方式是將線程池化(線程池),由線程池的調(diào)度策略決定線程們?nèi)绾螆?zhí)行提交給池中的任務(wù)哮洽,避免了重復(fù)構(gòu)造與銷毀線程的開銷填渠。上面我們提到,InheritableThreadLocal可以解決父線程向子線程傳遞線程私有數(shù)據(jù)的問題鸟辅,但一旦子線程池化之后氛什,InheritableThreadLocal也將不再起作用

ExecutorService executorService = Executors.newFixedThreadPool(1);
ThreadLocal<String> tl = new InheritableThreadLocal<>();

// ------------第一次調(diào)用 start -------------------
tl.set("foo");
executorService.execute(() -> {
    String str = tl.get();
    // 企圖拿到父線程放進(jìn)去的foo,成功了匪凉!
    System.out.println(str);
});
// 父線程A休息3秒枪眉,確保提交給線程池的任務(wù)執(zhí)行完畢
TimeUnit.SECONDS.sleep(3);
// 清理
tl.remove();
// ------------第一次調(diào)用 end -------------------



// ------------第二次調(diào)用 start -------------------
tl.set("bar");
executorService.execute(() -> {
    String str = tl.get();
    // 企圖拿到父線程放進(jìn)去的bar,失敗了再层!
    System.out.println(str);
});
// 父線程A休息3秒贸铜,確保提交給線程池的任務(wù)執(zhí)行完畢
TimeUnit.SECONDS.sleep(3);
// 清理
tl.remove();
// ------------第二次調(diào)用 end -------------------

如上,仍使用InheritableThreadLocal树绩,且構(gòu)造了只有1個(gè)線程的線程池,然后模擬兩次外部調(diào)用:第一次在父線程中將tl賦值為foo隐轩,然后子線程中獲取饺饭,使用完畢之后remove;第二次在父線程中將tl賦值為bar职车,然后子線程中獲取瘫俊,使用完畢之后remove

我們期望的結(jié)果是: foo bar

然而實(shí)際的結(jié)果卻是: foo foo

我們得到了與期望不符的結(jié)果鹊杖,原因很簡單,在局限性一解決方案原理中已經(jīng)闡述:子線程的ThreadLocalMap數(shù)據(jù)是在創(chuàng)建線程的那一刻從父線程中"拷貝"而來扛芽,此后再也沒有促使其變化的地方骂蓖,而子線程由于池化復(fù)用的緣故,ThreadLocalMap一直持有的是線程創(chuàng)建時(shí)刻的數(shù)據(jù)(即foo)川尖,此后無論進(jìn)行多少次方法調(diào)用登下,在(池化)子線程中通過tl.get()取出來的永遠(yuǎn)是foo

解決方案:使用transmittable-thread-local(alibaba)

暫不提開源框架是如何解決這個(gè)問題的,我們先自己推導(dǎo)叮喳,進(jìn)而得出與開源框架一致的結(jié)論被芳,幫助大家理好地理解解決方案背后的原理
仔細(xì)推敲琢磨一下,我們需要的并不是創(chuàng)建線程的那一刻父線程的ThreadLocal值馍悟,而是提交任務(wù)時(shí)父線程的ThreadLocal值畔濒。或者換種表述方式锣咒,需要把任務(wù)提交給線程池時(shí)的ThreadLocal值傳遞到任務(wù)執(zhí)行時(shí)
具體思路是:父線程把任務(wù)提交給線程池時(shí)一同附上此刻自己的ThreadLocalMap侵状,包裝在task里,待線程池中某個(gè)線程執(zhí)行到該任務(wù)時(shí)毅整,用task里的ThreadLocalMap賦蓋當(dāng)前線程ThreadLocalMap趣兄,這樣就完成了父線程向池化的子線程傳遞線程私有數(shù)據(jù)的目標(biāo)。為了避免數(shù)據(jù)污染毛嫉,待任務(wù)執(zhí)行完后诽俯,線程歸還回線程池之前,還需要還原ThreadLocalMap承粤,如下示:

image.png

上面的步驟一共有6步暴区,其中2、4辛臊、6是線程池本身的提供的能力仙粱,不需要改動(dòng),只有1彻舰、3伐割、5有所不同,我們逐一剖析

第1步疑問:提交Task的時(shí)候如何將ThreadLocalMap一同提交上去刃唤?此處難點(diǎn)在于如何獲取當(dāng)前線程的ThreadLocalMap

ThreadLocalMap是線程私有變量隔心,只會(huì)被ThreadLocal維護(hù),對于外部類而言是不可見的尚胞,因此要操作ThreadLocalMap就得通過ThreadLocal(操作ThreadLocal本質(zhì)上是在操作當(dāng)前線程的ThreadLocalMap)

首先自定義Task硬霍,用于包裝維護(hù)父線程ThreadLocalMap

public class MyTask implements Runnable {
        // key是ThreadLocal,value是對應(yīng)父線程的線程私有數(shù)據(jù)
    private final Map<ThreadLocal<Object>, Object> threadLocalValues;

    public MyTask(ThreadLocal<Object>... threadLocals) {
        this.threadLocalValues = new HashMap<>(threadLocals.length);
        capture(threadLocals);
    }

    private void capture(ThreadLocal<Object>[] threadLocals) {
        for (ThreadLocal<Object> threadLocal : threadLocals) {
            threadLocalValues.put(threadLocal, threadLocal.get());
        }
    }

    @Override
    public void run() {
        // todo
    }
}

使用時(shí):

ThreadLocal<Object> tl1 = new ThreadLocal<>();
tl1.set("111111");

ThreadLocal<Object> tl2 = new ThreadLocal<>();
tl2.set("222222");


ExecutorService executorService = Executors.newFixedThreadPool(1);
// 將父線程的ThreadLocal傳進(jìn)去笼裳,就能取到相應(yīng)的值
executorService.execute(new MyTask(tl1, tl2));

tl1.remove();
tl2.remove();

如此這般唯卖,提交到線程池的MyTask就包含了父線程的ThreadLoalMap數(shù)據(jù)粱玲。我們把"拷貝"父線程TheadLocalMap的行為稱為capture(拍照),一個(gè)很生動(dòng)形象的詞:將父線程提交任務(wù)時(shí)刻的ThreadLocal值拍個(gè)快照并保存起來拜轨,后續(xù)使用

第3步疑問:如何用父線程的ThreadLocalMap覆蓋當(dāng)前執(zhí)行任務(wù)線程的ThreadLocalMap抽减?

我們可以想像到,代碼正執(zhí)行到MyTask#run()方法橄碾,在該方法內(nèi)部卵沉,能感知的上下文環(huán)境是正執(zhí)行該方法的線程,以及MyTask維護(hù)的threadLocalValues(快照)堪嫂,除此之外偎箫,它獲取不了任何外界信息–> 這稱之為線程封閉

因此可以比較自然地推理出,是要用MyTask的threadLocalValues(快照)去覆蓋當(dāng)前線程的ThreadLocalMap皆串。我們稱這個(gè)動(dòng)作為replay(重放)

public class MyTask implements Runnable {
   // ...(省略)

    @Override
    public void run() {
        replay();
        // do biz
    }

    // 重放淹办,用快照去覆蓋當(dāng)前線程的ThreadLocalMap
    private void replay() {
        for (Map.Entry<ThreadLocal<Object>, Object> entry : threadLocalValues.entrySet()) {
            ThreadLocal<Object> threadLocal = entry.getKey();
            threadLocal.set(entry.getValue());
        }
    }
}

第5步疑問:如何把當(dāng)前線程的ThreadLocalMap還原?

Task業(yè)務(wù)邏輯執(zhí)行完之后恶复,毫無疑問需要將ThreadLocalMap還原怜森,否則可能產(chǎn)生數(shù)據(jù)污染的風(fēng)險(xiǎn)

能夠還原的前提是,用快照去覆蓋當(dāng)前線程的ThreadLocalMap之前谤牡,先將當(dāng)前的ThreadLocal值保存起來副硅,因此,修改代碼如下:

private Object replay() {
        // 保存當(dāng)前的ThreadLocal值
    Map<ThreadLocal<Object>, Object> backup = new HashMap<>();
    for (ThreadLocal<Object> threadLocal : threadLocalValues.keySet()) {
        backup.put(threadLocal, threadLocal.get());
    }

    for (Map.Entry<ThreadLocal<Object>, Object> entry : threadLocalValues.entrySet()) {
        ThreadLocal<Object> threadLocal = entry.getKey();
        threadLocal.set(entry.getValue());
    }

    return backup;
}

業(yè)務(wù)代碼執(zhí)行完之后翅萤,進(jìn)行restore(還原):

public class MyTask implements Runnable {
    // ...(省略)
    
    @Override
    public void run() {
        Object backup = replay();
        try {
            // do biz
        } finally {
            restore(backup);
        }
    }
        
        // 還原
    private void restore(Object obj) {
        Map<ThreadLocal<Object>, Object> backup = (Map<ThreadLocal<Object>, Object>) obj;

        for (Map.Entry<ThreadLocal<Object>, Object> entry : backup.entrySet()) {
            ThreadLocal<Object> threadLocal = entry.getKey();
            threadLocal.set(entry.getValue());
        }
    }
}

經(jīng)過上面的1恐疲、3、5步分析套么,我們已經(jīng)把所有關(guān)鍵問題都分析完畢培己,因此,做一個(gè)小實(shí)驗(yàn)看看結(jié)果胚泌,測試代碼:

ExecutorService executorService = Executors.newFixedThreadPool(1);
ThreadLocal<Object> tl1 = new ThreadLocal<>();
ThreadLocal<Object> tl2 = new ThreadLocal<>();


// ------------第一次調(diào)用 start -------------------
tl1.set("1111");
tl2.set("2222");

executorService.execute(new MyTask(tl1, tl2));

tl1.remove();
tl2.remove();
// ------------第一次調(diào)用 end -------------------


// ------------第二次調(diào)用 start -------------------
tl1.set("3333");
tl2.set("4444");

executorService.execute(new MyTask(tl1, tl2));

tl1.remove();
tl2.remove();
// ------------第二次調(diào)用 end -------------------
public void run() {
    Object backup = replay();
    try {
        // do biz
        doBiz();
    } finally {
        restore(backup);
    }
}

private void doBiz() {
        // 打印父線程提交任務(wù)時(shí)的ThreadLocal值
    Set<ThreadLocal<Object>> threadLocals = threadLocalValues.keySet();
    for (ThreadLocal<Object> threadLocal : threadLocals) {
        System.out.println(threadLocal.get());
    }
}

證明是可行的
但是這樣還不夠省咨,稍稍有些經(jīng)驗(yàn)的朋友應(yīng)該能感受到,上面的寫法僅是達(dá)到了"可用"的程度玷室,離"好用"還有一段距離:業(yè)務(wù)在向線程池提交任務(wù)的時(shí)候零蓉,需要每次都構(gòu)建自定義的Task,并將ThreadLocal的引用傳入穷缤,而且Task糅進(jìn)了TheadLocal管理的邏輯敌蜂,這樣其實(shí)形成了"業(yè)務(wù)侵入性",沒有做到與業(yè)務(wù)解耦津肛,這樣的代碼是不可維護(hù)的

TheadLocal管理的邏輯章喉,業(yè)務(wù)代碼不應(yīng)該關(guān)心,因此為了與業(yè)務(wù)解耦,容易想到的一種解決方案是:代理囊陡。通過代理可以實(shí)現(xiàn)對被代理類的邏輯增強(qiáng),并將通用的非業(yè)務(wù)邏輯與業(yè)務(wù)代碼隔離開來(設(shè)計(jì)模式縈繞心頭)

提及代理掀亥,熟悉的同學(xué)對于套路了然于心:定義代理類撞反,實(shí)現(xiàn)與被代理類相同的接口,并在內(nèi)部維護(hù)被代理類的實(shí)例搪花,之后就可以對被代理的方法實(shí)現(xiàn)額外邏輯遏片,來增強(qiáng)被代理類,代碼變更后如下:

public class MyTask implements Runnable {
    private Runnable task;
    private final Map<ThreadLocal<Object>, Object> threadLocalValues;

    public MyTask(Runnable task, ThreadLocal<Object>... threadLocals) {
        this.task = task;
        this.threadLocalValues = new HashMap<>(threadLocals.length);
        capture(threadLocals);
    }

    // ...(省略)

    @Override
    public void run() {
        Object backup = replay(); // 增強(qiáng)的邏輯
        try {
            task.run(); // 執(zhí)行業(yè)務(wù)代碼
        } finally {
            restore(backup); // 增強(qiáng)的邏輯
        }
    }
}

使用方式

executorService.execute(new MyTask(() -> {
    // do biz
}, tl1, tl2));

這樣提交任務(wù)時(shí)撮竿,業(yè)務(wù)開發(fā)只需要關(guān)心()->{// do biz} 業(yè)務(wù)邏輯吮便,而不需要關(guān)心MyTask代理類如何實(shí)現(xiàn)的增強(qiáng)邏輯,做到了與業(yè)務(wù)代碼的解耦

但是幢踏,上面的代碼書寫起來仍然有些別扭:需要業(yè)務(wù)方主動(dòng)傳遞tl1髓需、tl2,說明業(yè)務(wù)方仍然需要有一定程度的參與房蝉,那能不能更徹底一些僚匆,連傳遞tl1、tl2的行為都省去呢搭幻?答案是肯定的

如果要避免父線程"主動(dòng)"傳遞ThreadLocal的行為咧擂,那么就必須要知道父線程往ThreadLocalMap放數(shù)據(jù)這件事,并且在事件發(fā)生的時(shí)候?qū)hreadLocal引用保存下來檀蹋;同時(shí)松申,如果父線程調(diào)用ThreadLocal#remove方法清除數(shù)據(jù),也需要將保存下來的ThreadLocal引用一同清除掉

因此俯逾,需要自定義ThreadLocal類:

public class MyThreadLocal<T> extends ThreadLocal<T> {

    private static MyThreadLocal<HashSet<MyThreadLocal<Object>>> holder =
            new MyThreadLocal<HashSet<MyThreadLocal<Object>>>() {
                @Override
                protected HashSet<MyThreadLocal<Object>> initialValue() {
                    return new HashSet<>();
                }
            };

    // 重寫set方法
    @Override
    public void set(T value) {
        super.set(value);
        addThisToHolder();
    }
        
        // 將ThreadLocal引用記錄下來
    private void addThisToHolder() {
        if (!holder.get().contains(this)) {
            holder.get().add((MyThreadLocal<Object>) this);
        }
    }
    
    // 重寫remove方法
    @Override
    public void remove() {
        super.remove();
        removeThisFromHolder();
    }
    
        // 將ThreadLocal引用記錄移除
    private void removeThisFromHolder() {
        holder.get().remove(this);
    }
}

重寫set方法贸桶,在ThreadLocal#set方法執(zhí)行時(shí)將ThreadLocal引用記錄下來,保存在類成員變量holder中纱昧;重寫remove方法刨啸,在ThreadLocal#remove方法執(zhí)行時(shí)將ThreadLocal引用一并移除
接下來,父線程向線程池提交Task识脆,不再傳遞ThreadLocal的引用设联,那又怎么完成capture的動(dòng)作呢?(如果看官們忘記了灼捂,請往上翻离例,前文在執(zhí)行capture方法時(shí),入?yún)⑹荰hreadLocal引用數(shù)組)

既然我們將ThreadLocal的引用保存在MyThreadLocal#holder這個(gè)靜態(tài)變量中悉稠,那我們想辦法暴露holder宫蛆,不就可以得到capture需要的入?yún)⒘藛幔?/p>

image.png

這種思路誠然是可行的,但從面向?qū)ο笤O(shè)計(jì)角度而言卻不是最優(yōu)的:holder是MyThreadLocal的靜態(tài)成員變量,維護(hù)的數(shù)據(jù)是ThreadLocal集合耀盗,它不應(yīng)該將自身數(shù)據(jù)暴露出去想虎,而是遵循高內(nèi)聚的設(shè)計(jì)原則,提供數(shù)據(jù)操作的能力(方法)叛拷,例如提供capture的能力舌厨,但操作本身需要維護(hù)在類內(nèi)部。因此應(yīng)該是MyThreadLocal提供capture的能力忿薇,然后由需求方(MyTask)進(jìn)行調(diào)用裙椭。代碼如下:

public class MyThreadLocal<T> extends ThreadLocal<T> {

    // ...(省略)

    public static class DataTransmit {

        public static Map<ThreadLocal<Object>, Object> capture() {
            Map<ThreadLocal<Object>, Object> threadLocalValues = new HashMap<>();
            for (MyThreadLocal<Object> threadLocal : holder.get()) {
                threadLocalValues.put(threadLocal, threadLocal.get());
            }
            return threadLocalValues;
        }
    }
}

此處,我在MyThreadLocal中定義了一個(gè)內(nèi)部類DataTransmit署浩,用于ThreadLocal的數(shù)據(jù)傳輸揉燃,與MyThreadLocal本身提供的能力相隔離(SRP原則)。然后筋栋,將原先定義于MyTask的capture方法搬到了DataTransmit類內(nèi)炊汤,提供capture的能力。此時(shí)弊攘,MyTask構(gòu)造函數(shù)代碼如下:

public MyTask(Runnable task) {
    this.task = task;
    threadLocalValues = MyThreadLocal.DataTransmit.capture();
}

我們將capture方法搬走之后婿崭,仍然還有replay、restore方法肴颊,仔細(xì)思考氓栈,它們都是對ThreadLocal的操作,放在MyThreadLocal.DataTransmit中更合適一些婿着,使得內(nèi)聚度更高

最終授瘦,MyThreadLocal代碼如下

public class MyThreadLocal<T> extends ThreadLocal<T> {

    private static MyThreadLocal<HashSet<MyThreadLocal<Object>>> holder =
            new MyThreadLocal<HashSet<MyThreadLocal<Object>>>() {
                @Override
                protected HashSet<MyThreadLocal<Object>> initialValue() {
                    return new HashSet<>();
                }
            };


    @Override
    public void set(T value) {
        super.set(value);
        addThisToHolder();
    }

    private void addThisToHolder() {
        if (!holder.get().contains(this)) {
            holder.get().add((MyThreadLocal<Object>) this);
        }
    }

    @Override
    public void remove() {
        super.remove();
        removeThisFromHolder();
    }

    private void removeThisFromHolder() {
        holder.get().remove(this);
    }
    public static class DataTransmit {

        public static Map<ThreadLocal<Object>, Object> capture() {
            Map<ThreadLocal<Object>, Object> threadLocalValues = new HashMap<>();
            for (MyThreadLocal<Object> threadLocal : holder.get()) {
                threadLocalValues.put(threadLocal, threadLocal.get());
            }
            return threadLocalValues;
        }

        // 重放,用快照去覆蓋當(dāng)前線程的ThreadLocalMap
        public static Object replay(Object obj) {
            Map<ThreadLocal<Object>, Object> threadLocalValues = (Map<ThreadLocal<Object>, Object>)obj;

            Map<ThreadLocal<Object>, Object> backup = new HashMap<>();
            for (ThreadLocal<Object> threadLocal : threadLocalValues.keySet()) {
                backup.put(threadLocal, threadLocal.get());
            }

            for (Map.Entry<ThreadLocal<Object>, Object> entry : threadLocalValues.entrySet()) {
                ThreadLocal<Object> threadLocal = entry.getKey();
                threadLocal.set(entry.getValue());
            }

            return backup;
        }

        public static void restore(Object obj) {
            Map<ThreadLocal<Object>, Object> backup = (Map<ThreadLocal<Object>, Object>) obj;

            for (Map.Entry<ThreadLocal<Object>, Object> entry : backup.entrySet()) {
                ThreadLocal<Object> threadLocal = entry.getKey();
                threadLocal.set(entry.getValue());
            }
        }
    }
}

MyTask經(jīng)過精簡后的代碼如下:

public class MyTask implements Runnable {
    private Runnable task;
    private final Map<ThreadLocal<Object>, Object> threadLocalValues;

    public MyTask(Runnable task) {
        this.task = task;
        threadLocalValues = MyThreadLocal.DataTransmit.capture();
    }

    @Override
    public void run() {
        Object backup = MyThreadLocal.DataTransmit.replay(threadLocalValues);
        try {
            task.run();
        } finally {
            MyThreadLocal.DataTransmit.restore(backup);
        }
    }
}

使用代碼如下:

ExecutorService executorService = Executors.newFixedThreadPool(1);
ThreadLocal<Object> tl1 = new MyThreadLocal<>();
ThreadLocal<Object> tl2 = new MyThreadLocal<>();


// ------------第一次調(diào)用 start -------------------
tl1.set("1111");
tl2.set("2222");

executorService.execute(new MyTask(() -> {
    // do biz
    System.out.println(tl1.get());
    System.out.println(tl2.get());
}));

tl1.remove();
tl2.remove();
// ------------第一次調(diào)用 end -------------------


// ------------第二次調(diào)用 start -------------------
tl1.set("3333");
tl2.set("4444");

executorService.execute(new MyTask(() -> {
    // do biz
    System.out.println(tl1.get());
    System.out.println(tl2.get());
}));

tl1.remove();
tl2.remove();
// ------------第二次調(diào)用 end -------------------

這樣業(yè)務(wù)代碼就簡潔了竟宋。
如果還是覺得上述使用姿勢有點(diǎn)麻煩:每次提交任務(wù)提完,都要構(gòu)造一個(gè)MyTask,能不能連這一步都省去丘侠,變成跟規(guī)常的寫法一致呢徒欣?

executorService.execute(() -> {
    // do biz
    System.out.println(tl1.get());
    System.out.println(tl2.get());
});

答案是肯定的,首先需要明確的一點(diǎn)是蜗字,要增強(qiáng)就意味著要代理打肝;接著稍稍轉(zhuǎn)變一下思路:既然不能對Task進(jìn)行代理,那么我們對線程池進(jìn)行代理增強(qiáng)挪捕,是不是也可以達(dá)到同樣的效果粗梭?

代理套路相信大家很熟悉了:

public class MyExecutorService implements ExecutorService {
    private ExecutorService executorService;

    public MyExecutorService(ExecutorService executorService) {
        this.executorService = executorService;
    }

    @Override
    public void execute(Runnable command) {
        executorService.execute(new MyTask(command)); // 在內(nèi)部進(jìn)行Task的代理
    }
    // ...(省略)
}

使用:

ExecutorService executorService = new MyExecutorService(Executors.newFixedThreadPool(1));

這樣,業(yè)務(wù)代碼又精簡了一步:只需要對線程池進(jìn)行代理一次即可级零,后續(xù)提交任務(wù)不需要手動(dòng)構(gòu)建MyTask

最后断医,如果連對線程池的代理都感覺稍顯麻煩,只想使用原生的姿勢,那就要請出尚方寶劍:Java Agent鉴嗤,在應(yīng)用啟動(dòng)之初對JDK代碼進(jìn)行修改斩启,植入代理邏輯,如此業(yè)務(wù)代碼不需要進(jìn)行其它改動(dòng)醉锅,就享有增強(qiáng)后的ExecutorService以及Task

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浇垦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子荣挨,更是在濱河造成了極大的恐慌,老刑警劉巖朴摊,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件默垄,死亡現(xiàn)場離奇詭異,居然都是意外死亡甚纲,警方通過查閱死者的電腦和手機(jī)口锭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來介杆,“玉大人鹃操,你說我怎么就攤上這事〈荷冢” “怎么了荆隘?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長赴背。 經(jīng)常有香客問我椰拒,道長,這世上最難降的妖魔是什么凰荚? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任燃观,我火速辦了婚禮,結(jié)果婚禮上便瑟,老公的妹妹穿的比我還像新娘缆毁。我一直安慰自己,他們只是感情好到涂,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布脊框。 她就那樣靜靜地躺著,像睡著了一般践啄。 火紅的嫁衣襯著肌膚如雪缚陷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天往核,我揣著相機(jī)與錄音箫爷,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虎锚,可吹牛的內(nèi)容都是我干的硫痰。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼窜护,長吁一口氣:“原來是場噩夢啊……” “哼效斑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起柱徙,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤缓屠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后护侮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體敌完,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年羊初,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滨溉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,809評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡长赞,死狀恐怖晦攒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情得哆,我是刑警寧澤脯颜,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站贩据,受9級特大地震影響伐脖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜乐设,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一讼庇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧近尚,春花似錦蠕啄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至格遭,卻和暖如春哈街,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拒迅。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工骚秦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留她倘,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓作箍,卻偏偏與公主長得像硬梁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子胞得,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容