java中ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal三者区别

本文主要介绍 ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal 三者之间区别、如何使用、什么场景使用以及对原理和源码的介绍。介绍原理的时候通过最直白、最易懂的语言争取让大家了解三者之间的区别,以及日常如何把他们使用起来

ThreadLocal

ThreadLocal 解决的是每个线程可以拥有自己线程的变量实例。可以从隔离的角度解决变量线程安全的问题。

举个例子:

用户登陆后将用户的信息保存到 ThreadLocal 中,ThreadLocal<User> 可以保存请求过来的信息。也就是下面在这一个线程中任何地方都可以访问到这个 ThreadLocal 中的变量。虽然这是一个全局的静态变量,但是当有多个个线程调用 UserContext.setUser() 方法的时候,多个线程的变量都会保存,多个线程之间不会被相互覆盖。
看下下面代码,在同一个 JVM 进程中,虽然只有一个静态变量 userHolder,但是
线程A调用:UserContext.setUser(userA)
同时线程B调用:UserContext.setUser(userB)
在线程A中调用 UserContext.getUser() 得到的结果是 userA
在线程B中调用 UserContext.getUser() 得到的结果是 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();
    }
}

实现原理:
ThreadLocal 是每个 Thread 都绑定一个 Map,线程之间不会互相干扰
这个Map 不是普通的 Map而是一个定制 Map:ThreadLocalMap。
这个Map 使用了使用“开放寻址法”中最简单的“线性探测法”解决散列冲突问题。这点是和我们平常使用的普通的 HashMap 不一样的。其中还是用了一个神奇的数字 HASH_INCREMENT= 0x61c88647,保证了一个完美的散列分布。具体可以参考斐波那契散列法的相关资料
我们看它的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所以产生了ThreadLocal的进化版本

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方法中getUser的返回值是为null的。如果把InheritableThreadLocal替换为ThreadLocal,那么new Thread中的
UserContext.getUser();返回值是NULL

原理

我们看 InheritableThreadLocal 的源码发现它并没有太多的方法,其实主要的代码是在 Thread 的 init() 方法中。

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

可以看到 inheritableThreadLocals 还是一个 ThreadLocalMap,只不过是在 Thread 的 init 方法中把父Thread的inheritableThreadLocals 变量 copy 了一份给自己。同样借助 ThreadLocalMap 子线程可以获取到父线程的所有变量。
根据它的实现,我们也可以看到它的缺点,就是 Thread的 init 方法是在线程构造方法中 copy的,也就是在线程复用的线程池中是没有办法使用的。

TransmittableThreadLocal

上面介绍了 ThreadLocal 可以同线程共享变量。InheritableThreadLocal 可以父子线程共享变量,那么我们经常使用的线程池如何使用 ThreadLocal 这样的功能呢?TransmittableThreadLocal 简称(TTL)是阿里开源的一款支持线程池的 ThreadLocal 组件。

举个例子:

TenantContext 的实现是使用的 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

原因如下:例子中使用了固定线程数量为1的固定线程池。第一次sumbit task的时候,线程池会创建线程,因为使用了 InheritableThreadLocal ,所以调用 Thread 的 init 方法的时候会取父线程的 inheritableThreadLocals. 所以第一打印可以打印出 #########, mzt_1。下面又使用了 TenantContext.setTenantId("mzt_" + 2); 方法,之后又 submit 了一个线程,但是这时候线程池里面已经又线程了所以不会重新create线程了,也不会调用 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);
    }

重新运行之后的结果:

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 的时候复制一份父线程数据,那么为了支持线程池就需要在 Thread 的 run 方法之前把父线程的数据 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 类了,它实现了 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方法之前复制了父线程的ThreadLocal变量。当线程执行时,调用 TtlRunnable run 方法,TtlRunnable 会从 AtomicReference 中获取出调用线程中所有的上下文,并把上下文给 TransmittableThreadLocal.Transmitter.replay 方法把上下文复制到当前线程。并把上下文备份。
当线程执行完,调用 TransmittableThreadLocal.Transmitter.restore 并把备份的上下文传入,恢复备份的上下文,把后面新增的上下文删除,并重新把上下文复制到当前线程。

延伸

我们发现通过对线程池包裹一层还是侵入性太强,不符合SOLID原则,不优雅。TTL也提供了通过agent方式接入的方法,具体可以在TTL官网看文档。这里就不介绍了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339