从TransmittableThreadLocal使用前调研(源码分析)

  1. 快速入手
  2. 源码设计解读
    2.1 TransmittableThreadLocal的原理
    2.2 holder变量的设计
    2.3 TTL为什么不继承ThreadLocal
    2.4 为什么会有restore步骤
  3. 使用事项
    3.1 是否存在线程安全问题
    -3.1.1 子线程修改TTL,父线程能否感应到TTL变化?
    -3.1.2 父线程修改TTL,子线程能否感应到TTL变化?
    -3.1.3 线程不安全的解决方案
    3.2 是否存在内存泄漏问题
    3.3 子线程中需要调用remove方法吗?
    3.4 主线程执行remove方法会清空子线程的TTL的值吗?
    3.5 如何创建出默认值的TTL
    3.6 SpringBoot启动时,调用TTL的get方法如何保证线程安全
    3.7 创建出安全的TTL的方式
  4. 相关文章

1. 快速入手

github地址:https://github.com/alibaba/transmittable-thread-local
pom依赖:

<dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>transmittable-thread-local</artifactId>
     <version>2.11.5</version>
     <scope>compile</scope>
</dependency>

业务使用:

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;
import com.tellme.po.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
@RestController
public class ThreadLocalController {

    ExecutorService executorService =
            TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
    public static TransmittableThreadLocal<String> l2 = new TransmittableThreadLocal<>();
    public static TransmittableThreadLocal<User> local = new TransmittableThreadLocal<>();


    @RequestMapping("/local/t1")
    public void t1() {
        //第一次使用线程池时前,创建多个ThreadLocal的值。
        local.set(new User("001", "sam"));
        l2.set("李白不偷蓝");
        executorService.execute(() -> {
            log.info("【/local/t1的add操作】子线程打印数据{}", local.get());
        });

        executorService.execute(() -> {
            log.info("【/local/t1的add操作】子线程打印数据{}", local.get());
        });

        local.remove();
        l2.remove();
    }

    @RequestMapping("/local/t4")
    public void t4() {
        executorService.execute(() -> {
            log.info("【/local/t4】子线程打印数据{}", l2.get());
        });
    }
    
}

2. 源码设计解读

2.1 TransmittableThreadLocal的原理

在设计模式上采用装饰器模式去增强Runnable等任务。在向线程池提交Runnable任务时,去装饰Runnable,将主线程的ThreadLocal通过构造方法传递到Runnable对象。然后在run()方法中设置进去。

注意点:TransmittableThreadLocal需要配套使用TtlExecutors

TtlRunnable源码分析

    @Override
    public void run() {
        /**
         * capturedRef是主线程传递下来的ThreadLocal的值。
         */
        Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        /**
         * 1.  backup(备份)是子线程已经存在的ThreadLocal变量;
         * 2. 将captured的ThreadLocal值在子线程中set进去;
         */
        Object backup = replay(captured);
        try {
            /**
             * 待执行的线程方法;
             */
            runnable.run();
        } finally {
            /**
             *  在子线程任务中,ThreadLocal可能发生变化,该步骤的目的是
             *  回滚{@code runnable.run()}进入前的ThreadLocal的线程
             */
            restore(backup);
        }
    }

即步骤总结为:

  1. 装饰Runnable,将主线程的TTL传入到TtlRunnable的构造方法中;
  2. 将子线程的TTL的值进行备份,将主线程的TTL设置到子线程中(value是对象引用,可能存在线程安全问题);
  3. 执行子线程逻辑;
  4. 删除子线程新增的TTL,将备份还原重新设置到子线程的TTL中;

详细解读见下文:

  • 2.4 为什么会有restore步骤
  • 3.1 是否存在线程安全问题
  • 3.3 子线程中需要调用remove方法吗?

2.2 holder变量的设计

疑问:主线程需要将TransmittableThreadLocal传递到子线程,那么主线程如何记录所有的TransmittableThreadLocal?

Thread中存在下面两个变量,存储的是ThreadLocal的值和InheritableThreadLocal的值。并且ThreadLocal和InheritableThreadLocal分别实现了get()方法,可以获取到对应的ThreadLocalMap。

    /* 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;
  • TransmittableThreadLocal继承ThreadLocal,那么便去threadLocals填充值;
  • TransmittableThreadLocal继承InheritableThreadLocal,那么便去inheritableThreadLocals填充值;

TransmittableThreadLocal无论继承哪个类,若只是将TransmittableThreadLocal传递到子线程(粒度小),那么就不能简单的使用get()方法。

于是:TransmittableThreadLocal需要重新创建一个线程级别的缓存,来记录某个线程所有的TransmittableThreadLocal对象。

线程级别的缓存——在TransmittableThreadLocal创建一个ThreadLocal。

源码位置:TransmittableThreadLocal

    // Note about the holder:
    // 1. holder self is a InheritableThreadLocal(a *ThreadLocal*).
    // 2. The type of value in the holder is WeakHashMap<TransmittableThreadLocal<Object>, ?>.
    //    2.1 but the WeakHashMap is used as a *Set*:
    //        - the value of WeakHashMap is *always null,
    //        - and be never used.
    //    2.2 WeakHashMap support *null* value.
    private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
            new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                    return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
                }

                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                    return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
                }
            };

这就是holder的由来:无法对Thread进行扩展,且只想将TransmittableThreadLocal传递到子线程。即自己去维护一个线程级别的全局缓存。

  1. static修饰:一个线程中,无论TransmittableThreadLocal被创建多少次,需要保证维护的是同一个缓存。即static保证;
  2. InheritableThreadLocal:这个后续展开,其实和单纯使用ThreadLocal区别不大;
  3. WeakHashMap:弱引用(发生GC便回收),这个是为了确保本框架不会造成潜在的内存泄漏;

2.3 TTL为什么不继承ThreadLocal

disable Inheritable when it's not necessary and buggy(eg. has potential memory leaking problem) #100

InheritableThreadLocal是一个潜在的泄漏问题。新创建的线程永远不知道如何处理继承的值。应在任务的生命周期内创建和清除可传输的线程本地值。InheritableThreadLocal 会将可传输线程本地值的管理泄漏到任务之外。

测试代码见:TransmittableThreadLocal会不会有内存泄漏的风险? #281

作者设计理念:

image.png

当然:TransmittableThreadLocal也是可以关闭Inheritable的。详见

2.4 为什么会有restore步骤

  1. Runnable可能由主线程执行,若Runnable修改了TransmittableThreadLocal,可能会造成主线程的TransmittableThreadLocal值变化,造成bug;
image.png
  1. restore中由框架进行了ttl的remove操作,无需开发人员在子线程中显式调用remove()方法,也不会造成内存泄漏。

代码位置:com.alibaba.ttl.TransmittableThreadLocal.Transmitter#restoreTtlValues

image.png

3. 使用事项

3.1 是否存在线程安全问题

异步线程中修改ThreadLocal值,主线程也会被修改?(DB Session传递场景) #253

TransmittableThreadLocalrestore的操作,但是子线程中修改了对象的引用,主线程是可以感知到的。可能存在线程安全问题。

image.png

3.1.1 子线程修改TTL,父线程能否感应到TTL变化?

默认是可以的,线程不安全问题的具体体现,解决方案详见3.1.3 线程不安全的解决方案

3.1.2 父线程修改TTL,子线程能否感应到TTL变化?

默认是可以的,线程不安全问题的具体体现,解决方案详见3.1.3 线程不安全的解决方案

3.1.3 线程不安全的解决方案

在restore方法中(详见2.4 为什么会有restore步骤),会remove掉子线程的TTL。但是在replay方法父子线程传递的是对象引用。父子线程修改对象便可能会导致线程不安全问题。

    public static TransmittableThreadLocal<Map<String, String>> l2 = new TransmittableThreadLocal<Map<String, String>>() {

        /**
         * 完成对象的copy,对于复杂的对象,可以采用序列化反序列化的方式进行深拷贝
         */
        @Override
        public Map<String, String> copy(Map<String, String> parentValue) {
            return new HashMap<>(parentValue);
        }
    };

注意:深拷贝的问题。JAVA基础篇(3)-深克隆与浅克隆

3.2 是否存在内存泄漏问题

TransmittableThreadLocal会不会有内存泄漏的风险? #281

Inheritable能力/功能 (即第一次创建Thread时的上下文传递)引发的问题。

解决方法 参见 #279 (comment)@yexuerui

TTL提供了 关闭 Inheritable能力的解法:
(可能要理解一下 这个解法及其背景原因,有些复杂性。 <g-emoji class="g-emoji" alias="smile" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f604.png" style="box-sizing: border-box; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 1.25em; font-weight: 400; line-height: 1; vertical-align: -0.075em; font-style: normal !important;">😄</g-emoji> )

3.3 子线程中需要调用remove方法吗?

不需要,结果详见2.4 为什么会有restore步骤

3.4 主线程执行remove方法会清空子线程的TTL的值吗?

不会。

主线程调用remove方法,将TTL对应的value的引用指向null。
但是TTL传递到子线程,子线程也会持有一个value的引用。依旧可以获取到TTL的值。

3.5 如何创建出默认值的TTL

  • 创建出的TTL的value默认值不为null,可以重写initialValue方法;
  • 因为TTL继承与ITL,也可以重写childValue方法,实现new Thread()时父线程不向子线程传递变量;当然官方推荐使用DisableInheritableThreadFactory去装饰我们线程池的ThreadFactory;
    public static TransmittableThreadLocal<Map<String, String>> l2 = new TransmittableThreadLocal<Map<String, String>>() {
        @Override
        protected Map<String, String> initialValue() {
            return new HashMap<>();
        }
        //防止ITL的潜在的内存泄漏(官方也提供对应的API去实现)可以不重写方法。
        protected Map<String, String> childValue(Map<String, String> parentValue) {
            return initialValue();
        }
    };

3.6 SpringBoot启动时,调用TTL的get方法如何保证线程安全

https://github.com/alibaba/transmittable-thread-local/issues/282

因为TTL底层使用ITL,会导致在new线程的时候,父子线程的数据传递,且无法销毁。

背景:

  1. 项目启动的时候,存在TTL的get操作,于是main线程存在TTL的value;
  2. 当请求进入时,Tomcat线程池(不会被TtlExecutors装饰)会开启子线程来执行业务逻辑;
  3. main线程会将TTL(此时仅可看做ITL)的值传递到子线程;
  4. 子线程修改TTL的引用时,会造成内存不安全;
    代码如下:
@Slf4j
@RestController
public class ThreadLocalController {

    ExecutorService executorService =
            TtlExecutors.getTtlExecutorService(new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>()));

    public static TransmittableThreadLocal<Map<String, String>> l2 = new TransmittableThreadLocal<Map<String, String>>() {
        @Override
        protected Map<String, String> initialValue() {
            return new HashMap<>();
        }
    };

    /**
     * 项目启动的时候,会调用TTL的get方法,这里使用static模拟;
     */
    static {
        l2.get();
        log.info("项目启动时加载配置");
    }


    @RequestMapping("/local/t1")
    public void t1() throws InterruptedException {
        Map<String, String> mc = l2.get();
        mc.put("t1", "t1v");
        log.info("【/local/t1】主线程打印:" + l2.get());
        executorService.execute(() -> {
            log.info("【/local/t1】子线程2map{}", l2.get());
        });
        Thread.sleep(1000);
        l2.remove();
    }

    @RequestMapping("/local/t4")
    public void t4() {
        log.info("【/local/t4】主线程打印:" + l2.get());
        executorService.execute(() -> {
            log.info("【/local/t4】子线程打印数据{}", l2.get());
        });
        Map<String, String> cache = l2.get();
        cache.put("l4", "l4v");
        l2.remove();
    }

}

疑问:此时由于是普通的线程池,即使TTL重写copy方法也会造成线程不安全;

解决方法只有去重写childValue方法,来解决ITL传递到子线程吗?:

    public static TransmittableThreadLocal<Map<String, String>> l2 = new TransmittableThreadLocal<Map<String, String>>() {
        @Override
        protected Map<String, String> initialValue() {
            return new HashMap<>();
        }

        @Override
        protected Map<String, String> childValue(Map<String, String> parentValue) {
            return initialValue();
        }
    };
image.png

3.7 创建出安全的TTL的方式

    /**
     * 环境变量数据
     */
    private static final TransmittableThreadLocal<Map<String, String>> sealThreadLocalEnv = new TransmittableThreadLocal<Map<String, String>>() {
        @Override
        protected Map<String, String> initialValue() {
            return new LinkedHashMap<>();
        }
        //解决普通线程池中使用TTL,造成数据污染的问题
        @Override
        protected Map<String, String> childValue(Map<String, String> parentValue) {
            return initialValue();
        }
       //父子线程使用的是拷贝对象。而非简单对象的引用。
        @Override
        public Map<String, String> copy(Map<String, String> parentValue) {
            return new LinkedHashMap<>(parentValue);
        }
    };

4. 相关文章

JAVA进阶篇(8)—TransmittableThreadLocal—父子线程间线程本地变量

JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!

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

推荐阅读更多精彩内容