ThreadLocal及其扩展

ThreadLocal使用

用于相同线程内上下文的传递,避免显式传参,简化代码。
比如controller层把用户信息set到ThreadLocal,在service层get获取,无需显式传参。

ThreadLocal原理

Thread类中有一个threadLocals变量,类型为ThreadLocalMap

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

ThreadLocalMap是一个自定义的hash map,底层是Entry数组,提供了getEntry(ThreadLocal<?> key) 、set(ThreadLocal<?> key, Object value)等api,使用线性探测法处理hash冲突。
从api可以看出,ThreadLocalMap的key是ThreadLocal对象,value是泛型对象值。

ThreadLocal的值,存储在Thread对象实例的堆内存空间里,而不是ThreadLocal对象,ThreadLocal只是提供了get set方法用于维护变量值。

InheritableThreadLocal使用

Thread类中,除了有threadLocals变量,还有一个inheritableThreadLocals变量

public class Thread {
    /* 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;
}

假设这样一个场景: 父线程开了一个子线程,但是我们希望在子线程中可以访问父线程中的ThreadLocal对象,也就是说有些数据需要进行父子线程间的传递。比如像这样:

public static void main(String[] args) {
    ThreadLocal<Integer> threadLocal = new ThreadLocal();

    for (int i = 0; i < 10; i++) {
        // 父线程set
        threadLocal.set(i);

        new Thread(() -> {
            // 子线程get
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        }).start();
    }
}

执行结果

Thread-0: null
Thread-2: null
Thread-1: null
Thread-4: null
Thread-3: null
Thread-5: null
Thread-6: null
Thread-7: null
Thread-8: null
Thread-9: null

如果我们希望子线可以看到父线程的ThreadLocal,那么就可以使用InheritableThreadLocal

public static void main(String[] args) {
    ThreadLocal<Integer> threadLocal = new InheritableThreadLocal();

    for (int i = 0; i < 10; i++) {
        // 父线程set
        threadLocal.set(i);

        new Thread(() -> {
            // 子线程get
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        }).start();
    }
}

再次执行

Thread-0: 0
Thread-2: 2
Thread-1: 1
Thread-3: 3
Thread-4: 4
Thread-5: 5
Thread-6: 6
Thread-7: 7
Thread-8: 8
Thread-9: 9

可以看到,每个子线程都可以访问到从父线程传递过来的一个数据。

InheritableThreadLocal原理

InheritableThreadLocal源码如下,可以看到InheritableThreadLocal重写了父类的几个方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    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);
    }
}

重写createMap方法是为了把变量存到Thread里的inheritableThreadLocals变量,而不是threadLocals变量。

Thread类的构造方法,会调用init方法。
调用子线程Thread类的init方法时,如果parent.inheritableThreadLocals不为空,会把parent.inheritableThreadLocals浅拷贝到子线程的inheritableThreadLocals.

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

实际业务里,InheritableThreadLocal的使用场景很少,主要原因如下

  1. 上下文的拷贝是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不行了,因为线程池的核心线程是提前创建的
  2. 父子线程的map里的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题(父子线程同时访问)

TransmittableThreadLocal原理

阿里TransmittableThreadLocal依赖

<!-- https://mvnrepository.com/artifact/com.alibaba/transmittable-thread-local -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.5</version>
</dependency>

如果使用线程池,有3个时机需要考虑: 创建子线程、提交任务、执行任务

  1. 线程池的核心线程在线程池初始化时就创建了,创建时机远远早于用户请求。
  2. 提交任务后,不一定会马上执行,有可能核心线程数用完了,需要在阻塞队列里等待。
  3. 最好是在执行任务前,把父线程的上下文拷贝到子线程。

假设一个请求打到tomcat,tomcat从线程池里取出线程A(父线程),在controller层设置了用户信息到线程A的ThreadLocal。然后进入service层,使用线程池,从线程池里拿出线程B执行业务逻辑。

线程A提交任务后,就可以处理别的请求了。

如果等到线程B执行任务前,才从线程A拷贝上下文,这时候线程A的ThreadLocal可能被其他请求的数据覆盖了。

因此,线程A提交任务时,就需要把上下文拷贝到一个地方,然后线程B执行任务前,再从那个地方拷贝上下文。

总结

ThreadLocal: 无法复制上下文至子线程中
InheritableThreadLocal: 创建子线程时,从父线程复制上下文
TransmittableThreadLocal: 子线程执行任务时,从父线程复制上下文

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

推荐阅读更多精彩内容