java中ThreadLocal孟害、InheritableThreadLocal拒炎、TransmittableThreadLocal三者區(qū)別

本文主要介紹 ThreadLocal、InheritableThreadLocal挨务、TransmittableThreadLocal 三者之間區(qū)別击你、如何使用玉组、什么場景使用以及對原理和源碼的介紹。介紹原理的時候通過最直白丁侄、最易懂的語言爭取讓大家了解三者之間的區(qū)別惯雳,以及日常如何把他們使用起來

ThreadLocal

ThreadLocal 解決的是每個線程可以擁有自己線程的變量實例『枰。可以從隔離的角度解決變量線程安全的問題石景。

舉個例子:

用戶登陸后將用戶的信息保存到 ThreadLocal 中,ThreadLocal<User> 可以保存請求過來的信息拙吉。也就是下面在這一個線程中任何地方都可以訪問到這個 ThreadLocal 中的變量潮孽。雖然這是一個全局的靜態(tài)變量,但是當(dāng)有多個個線程調(diào)用 UserContext.setUser() 方法的時候筷黔,多個線程的變量都會保存往史,多個線程之間不會被相互覆蓋。
看下下面代碼佛舱,在同一個 JVM 進(jìn)程中椎例,雖然只有一個靜態(tài)變量 userHolder,但是
線程A調(diào)用:UserContext.setUser(userA)
同時線程B調(diào)用:UserContext.setUser(userB)
在線程A中調(diào)用 UserContext.getUser() 得到的結(jié)果是 userA
在線程B中調(diào)用 UserContext.getUser() 得到的結(jié)果是 userB

public class UserContext {
    private UserContext() {
    }
    private static ThreadLocal<User> userHolder = new ThreadLocal<>();
    public static User getUser() {
        return userHolder.get();
    }
    public static void setUser(User user) {
        userHolder.set(user);
    }
    public static void clean() {
        userHolder.remove();
    }
}

實現(xiàn)原理:
ThreadLocal 是每個 Thread 都綁定一個 Map名眉,線程之間不會互相干擾
這個Map 不是普通的 Map而是一個定制 Map:ThreadLocalMap粟矿。
這個Map 使用了使用“開放尋址法”中最簡單的“線性探測法”解決散列沖突問題。這點是和我們平常使用的普通的 HashMap 不一樣的损拢。其中還是用了一個神奇的數(shù)字 HASH_INCREMENT= 0x61c88647,保證了一個完美的散列分布撒犀。具體可以參考斐波那契散列法的相關(guān)資料
我們看它的get源碼:

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

從上面get源碼的前兩行可以看到福压,ThreadLocal的缺點:它不支持子線程。因為map是綁定在currentThread中的或舞。子線程和父線程并不是一個Thread所以產(chǎn)生了ThreadLocal的進(jìn)化版本

InheritableThreadLocal

前面說到ThreadLocal并不支持子線程荆姆,InheritableThreadLocal就是支持子線程的ThreadLocal

舉個例子

public class UserContext {
    private UserContext() {
    }
    private static InheritableThreadLocal<User> userHolder = new InheritableThreadLocal<>();
    public static User getUser() {
        return userHolder.get();
    }
    public static void setUser(User user) {
        userHolder.set(user);
    }
    public static void clean() {
        userHolder.remove();
    }
}


@Test
    public void test3() throws InterruptedException {
        UserEntity userEntity = new UserEntity();
        userEntity.setUsername("mzt");
        UserContext.setUser(userEntity);

        UserEntity user = UserContext.getUser();
        Assert.assertNotNull(user);
        Assert.assertEquals(user.getUsername(), "mzt");
        new Thread(() -> {
            final UserEntity user1 = UserContext.getUser();
            Assert.assertNotNull(user1);
            Assert.assertEquals(user1.getUsername(), "mzt");
        }).start();
        TimeUnit.MINUTES.sleep(1);
    }

因為使用了InheritableThreadLocal這時候兩個Assert都是正確的。但是僅僅使用ThreadLocal的時候上面例子中new Threa的run方法中g(shù)etUser的返回值是為null的映凳。如果把InheritableThreadLocal替換為ThreadLocal胆筒,那么new Thread中的
UserContext.getUser();返回值是NULL

原理

我們看 InheritableThreadLocal 的源碼發(fā)現(xiàn)它并沒有太多的方法,其實主要的代碼是在 Thread 的 init() 方法中诈豌。

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

可以看到 inheritableThreadLocals 還是一個 ThreadLocalMap仆救,只不過是在 Thread 的 init 方法中把父Thread的inheritableThreadLocals 變量 copy 了一份給自己。同樣借助 ThreadLocalMap 子線程可以獲取到父線程的所有變量矫渔。
根據(jù)它的實現(xiàn)彤蔽,我們也可以看到它的缺點,就是 Thread的 init 方法是在線程構(gòu)造方法中 copy的庙洼,也就是在線程復(fù)用的線程池中是沒有辦法使用的顿痪。

TransmittableThreadLocal

上面介紹了 ThreadLocal 可以同線程共享變量镊辕。InheritableThreadLocal 可以父子線程共享變量,那么我們經(jīng)常使用的線程池如何使用 ThreadLocal 這樣的功能呢蚁袭?TransmittableThreadLocal 簡稱(TTL)是阿里開源的一款支持線程池的 ThreadLocal 組件征懈。

舉個例子:

TenantContext 的實現(xiàn)是使用的 InheritableThreadLocal

public class TenantContext {
    private static InheritableThreadLocal<String> tenantHolder = new InheritableThreadLocal<>();
// 其他方法略....
}
 @Test
    public void test1() throws InterruptedException {
        ExecutorService ttlExecutorService = Executors.newFixedThreadPool(1);
        TenantContext.setTenantId("mzt_" + 1);
        ttlExecutorService.submit(() -> {
            String tenantId = TenantContext.getTenantId();
            log.info("#########, {}", tenantId);
            //TenantContext.clean();
        });
        TimeUnit.SECONDS.sleep(2);
        TenantContext.setTenantId("mzt_" + 2);
        ttlExecutorService.submit(() -> {
            log.info("#########, {}", TenantContext.getTenantId());
           // TenantContext.clean();
        });

        Thread.sleep(2000L);
    }

上面的例子中兩次輸出都是什么呢?

2021/01/24 21:49:24.678 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:77) #########, mzt_1
2021/01/24 21:49:26.671 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:83) #########, mzt_1

原因如下:例子中使用了固定線程數(shù)量為1的固定線程池揩悄。第一次sumbit task的時候卖哎,線程池會創(chuàng)建線程,因為使用了 InheritableThreadLocal 虏束,所以調(diào)用 Thread 的 init 方法的時候會取父線程的 inheritableThreadLocals. 所以第一打印可以打印出 #########, mzt_1棉饶。下面又使用了 TenantContext.setTenantId("mzt_" + 2); 方法,之后又 submit 了一個線程镇匀,但是這時候線程池里面已經(jīng)又線程了所以不會重新create線程了照藻,也不會調(diào)用 thread 的 init 方法了,所以 get 出來的變量還是 mzt1 汗侵。
那么下面改成阿里的TTL

public class TenantContext {
    private static TransmittableThreadLocal<String> tenantHolder = new TransmittableThreadLocal<>();
// 其他方法略....
}

@Test
    public void test1() throws InterruptedException {
        ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
        TenantContext.setTenantId("mzt_" + 1);
        ttlExecutorService.submit(() -> {
            String tenantId = TenantContext.getTenantId();
            log.info("#########, {}", tenantId);
            //TenantContext.clean();
        });
        TimeUnit.SECONDS.sleep(2);
        TenantContext.setTenantId("mzt_" + 2);
        ttlExecutorService.submit(() -> {
            log.info("#########, {}", TenantContext.getTenantId());
           // TenantContext.clean();
        });

        Thread.sleep(2000L);
    }

重新運行之后的結(jié)果:

2021/01/24 21:58:58.751 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:77) #########, mzt_1
2021/01/24 21:59:00.746 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:83) #########, mzt_2

原理

我們看到普通的線程池子被 TtlExecutors.getTtlExecutorService() 包裹了一下幸缕。如果僅僅是吧InheritableThreadLocal 修改為 TransmittableThreadLocal 是不起作用的。
所以 TTL 的做法也比較直接晰韵,使用了裝飾器模式发乔,既然 InheritableThreadLocal 只是在線程Create 的時候復(fù)制一份父線程數(shù)據(jù),那么為了支持線程池就需要在 Thread 的 run 方法之前把父線程的數(shù)據(jù) copy 一下就可以了雪猪。從源碼中看是

 public static ExecutorService getTtlExecutorService(ExecutorService executorService) {
        if (executorService == null || executorService instanceof ExecutorServiceTtlWrapper) {
            return executorService;
        }
        return new ExecutorServiceTtlWrapper(executorService);
    }

重點:ExecutorServiceTtlWrapper包裝了我們普通的 ExecutorService栏尚,然后充血了 submit 方法

 @Override
    public <T> Future<T> submit(Runnable task, T result) {
        return executorService.submit(TtlRunnable.get(task), result);
    }

重點就是 TtlRunnable 類了,它實現(xiàn)了 Runable 方法只恨,并且重寫了 run 方法

 /**
     * wrap method {@link Runnable#run()}.
     */
    @Override
    public void run() {
        Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
        if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }

        Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
        try {
            runnable.run();
        } finally {
            TransmittableThreadLocal.restoreBackup(backup);
        }
    }

這個代碼就是流程的核心了译仗,就是在run方法之前復(fù)制了父線程的ThreadLocal變量。當(dāng)線程執(zhí)行時官觅,調(diào)用 TtlRunnable run 方法纵菌,TtlRunnable 會從 AtomicReference 中獲取出調(diào)用線程中所有的上下文,并把上下文給 TransmittableThreadLocal.Transmitter.replay 方法把上下文復(fù)制到當(dāng)前線程休涤。并把上下文備份咱圆。
當(dāng)線程執(zhí)行完,調(diào)用 TransmittableThreadLocal.Transmitter.restore 并把備份的上下文傳入功氨,恢復(fù)備份的上下文序苏,把后面新增的上下文刪除,并重新把上下文復(fù)制到當(dāng)前線程疑故。

延伸

我們發(fā)現(xiàn)通過對線程池包裹一層還是侵入性太強杠览,不符合SOLID原則,不優(yōu)雅纵势。TTL也提供了通過agent方式接入的方法踱阿,具體可以在TTL官網(wǎng)看文檔管钳。這里就不介紹了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末软舌,一起剝皮案震驚了整個濱河市才漆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌佛点,老刑警劉巖醇滥,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異超营,居然都是意外死亡鸳玩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門演闭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來不跟,“玉大人,你說我怎么就攤上這事米碰∥迅铮” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵吕座,是天一觀的道長虐译。 經(jīng)常有香客問我,道長吴趴,這世上最難降的妖魔是什么漆诽? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮锣枝,結(jié)果婚禮上拴泌,老公的妹妹穿的比我還像新娘。我一直安慰自己惊橱,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布箭昵。 她就那樣靜靜地躺著税朴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪家制。 梳的紋絲不亂的頭發(fā)上正林,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天,我揣著相機與錄音颤殴,去河邊找鬼觅廓。 笑死,一個胖子當(dāng)著我的面吹牛涵但,可吹牛的內(nèi)容都是我干的杈绸。 我是一名探鬼主播帖蔓,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瞳脓!你這毒婦竟也來了塑娇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤劫侧,失蹤者是張志新(化名)和其女友劉穎埋酬,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烧栋,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡写妥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了审姓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片珍特。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖邑跪,靈堂內(nèi)的尸體忽然破棺而出次坡,到底是詐尸還是另有隱情,我是刑警寧澤画畅,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布砸琅,位于F島的核電站,受9級特大地震影響轴踱,放射性物質(zhì)發(fā)生泄漏症脂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一淫僻、第九天 我趴在偏房一處隱蔽的房頂上張望诱篷。 院中可真熱鬧,春花似錦雳灵、人聲如沸棕所。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琳省。三九已至,卻和暖如春躲撰,著一層夾襖步出監(jiān)牢的瞬間针贬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工拢蛋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留桦他,地道東北人。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓谆棱,卻偏偏與公主長得像快压,于是被迫代替她去往敵國和親圆仔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359