为什么需要TransmittableThreadLocal

1. 正确的ThreadLocal的使用

1.1 如何使用

同一个线程中,通过ThreadLocal传递上下文,在使用后进行释放

1.2 使用场景

  • 示意图


    image.png
  • 示例说明
    • 如果web应用在处理tom的请求的时候,bob又进来,使用了线程t2, t1和t2的threadlocal变量是隔离的,也就是线程安全的
    • 线程使用完后将threadlocal释放,避免内存泄漏
    • 这种情况下,线程的thradlocal可以从上层流转到下层,上层的修改对下层是可见的

1.3 代码示例

package com.zihao.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author tangzihao
 * @date 2020/9/5 8:08 下午
 */
@RestController
public class TestThreadLocalController {
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    /**
     * 处理用户的请求
     * @param uid 用户的uid
     */
    @RequestMapping("/testThreadLocal")
    public void handleRequest(String uid){
        System.out.println("current user uid:"+uid);
        //将uid放入ThreadLocal中,通过ThreadLocal进行传值,避免共享成员变量产生线程不安全
        threadLocal.set(uid);
        query();
    }
    /**
     * 查询用户信息
     */
    private void query(){
        try{
            queryCache();
            queryDb();
        }finally {
            //用完后一定要释放该线程的threadLocal
            threadLocal.remove();
            //移除后输出下内容
            System.out.println("after remove,current threadLocal:"+threadLocal.get());
            System.out.println();
        }
    }
    /**
     * 模拟查询缓存
     */
    private void queryCache(){
        System.out.println("thread ["+Thread.currentThread().getName()+"] query cache, uid:"+threadLocal.get());
    }
    /**
     * 模拟查询数据库
     */
    private void queryDb(){
        System.out.println("thread ["+Thread.currentThread().getName()+"] query db, uid:"+threadLocal.get());
    }
}

我是在springboot项目中,简单写了个controller模拟用户的请求

1.4 输出结果

image.png

1.5 其他联想

  • tomcat的线程池的设计策略
    • tomcat的线程池是如何扩展jdk的线程池的,具体见tomcat源码的org.apache.tomcat.util.threads.ThreadPoolExecutor,tomcat源码编译本博客有可以看下喔
  • springboot内嵌的tomcat配置
    • 配置项见ServerProperties
  • Spring boot如何切换不同的web容器
    • 使用不同的web容器,线程池的命名是不同的
    • 容器可以切换成jetty和undertow
      • 排除spring-boot-starter-web的spring-boot-starter-tomcat
      • 引入spring-boot-starter-jetty或者spring-boot-starter-undertow

2. 父子线程传值的问题

2.1 示例代码

package com.test.zihao;
/**
 * 父子线程传递ThreadLocal变量
 *
 * @author tangzihao
 * @date 2020/9/5 9:42 下午
 */
public class ParentChildThreadLocalCase {
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        //main线程 设置threadLocal
        threadLocal.set("hello");
        Thread childThread = new Thread(() -> {
            System.out.println("current thread: " + Thread.currentThread().getName() + ", value from thread local: " + threadLocal.get());
        }, "childThread");
        childThread.start();
        System.out.println(Thread.currentThread().getName() + ", value from thread local: " + threadLocal.get());
    }
}

2.2 输出结果

main, value from thread local: hello
current thread: childThread, value from thread local: null

2.3 原因分析

  • ThreadLocal是对当前线程有效
  • 这个case中有两个线程
    • 线程1,main线程,main程序的执行线程,main线程设置了自己线程的threadLocal,只有main线程自己get才可以获取到之前它自己设置的值
    • 线程2,childThread线程,是main线程的子线程,子线程尝试获取父线程main的threadlocal变量,获取为null

3. InheritableThreadLocal解决父子线程的传值

3.1 示例代码

package com.test.zihao;
/**
 * 父子线程传递ThreadLocal变量
 *
 * @author tangzihao
 * @date 2020/9/5 9:42 下午
 */
public class ParentChildThreadLocalCase {
    private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        //main线程 设置threadLocal
        threadLocal.set("hello");
        Thread childThread = new Thread(() -> {
            System.out.println("current thread: " + Thread.currentThread().getName() + ", value from thread local: " + threadLocal.get());
        }, "childThread");
        childThread.start();
        System.out.println(Thread.currentThread().getName() + ", value from thread local: " + threadLocal.get());
    }
}

3.2 输出结果

current thread: childThread, value from thread local: hello
main, value from thread local: hello

3.3 结论

  • 使用InheritableThreadLocal可以在父子线程之间传递ThreadLocal变量
  • 子线程修改的InheritableThreadLocal对父线程不可见
  • 子线程之间的InheritableThreadLocal彼此不可见

3.4 InheritableThreadLocal传值的源码

(1) 如果父线程的inheritableThreadLocals不为null同时允许继承inheritThreadLocals的话,将父线程的inheritableThreadLocals复制到子线程
Thread源码的418行

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

(2) InheritableThreadLocal通过重写childValue方法来将父线程的值注入
ThreadLocal的ThreadLocalMap(ThreadLocalMap parentMap)方法

private ThreadLocalMap(ThreadLocalMap parentMap) {
            //构造table数组
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];
            
            //复制parentMap的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) {
                        //key.childValue childValue是InheritableThreadLocal重写的方法
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        //找到table中空的索引位置
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        //设置table对应索引位置的entry
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

InheritableThreadLocal line61

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

(3) 通过重写getMap方法,将threadLocal的get和set方法由线程的inheritableThreadLocals维护

ThreadLocalMap getMap(Thread t) {
   return t.inheritableThreadLocals;
}

4. 线程池复用的传值问题

4.1 问题简单描述

对于使用线程池等会池化复用线程的组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时

4.2 使用场景

  • 假设我有个需求需要记录程序执行的方法调用顺序,在方法调用中需要使用线程池进行并发调用
  • 执行示意图


    image.png

4.3 问题代码

package com.test.zihao;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 线程池使用threadLocal
 *
 * @author tangzihao
 * @date 2020/9/5 10:40 下午
 */
public class ThreadPoolUseInheritableThreadLocalCase {
    private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    private static ExecutorService executorService = Executors.newFixedThreadPool(2);
    public static void main(String[] args) {
        threadLocal.set("main-->");
        executorService.submit(ThreadPoolUseInheritableThreadLocalCase::queryShop);
        executorService.submit(ThreadPoolUseInheritableThreadLocalCase::queryItem);
        executorService.submit(ThreadPoolUseInheritableThreadLocalCase::queryCoupon);
    }
    /**
     * 查询店铺信息
     */
    private static void queryShop() {
        threadLocal.set(threadLocal.get() + "queryShop");
        record();
    }
    /**
     * 查询商品
     */
    private static void queryItem() {
        threadLocal.set(threadLocal.get() + "queryItem");
        record();
    }
    /**
     * 查询优惠券
     */
    private static void queryCoupon() {
        threadLocal.set(threadLocal.get() + "queryCoupon");
        record();
    }
    /**
     * 记录日志
     */
    private static void record() {
        threadLocal.set(threadLocal.get() + "-->record");
        System.out.println(Thread.currentThread().getName() + " method call chain[ " + threadLocal.get() + " ]");
    }
}

4.4 输出结果

明显的发现在复用pool-1-thread-1的时候,无法获取提交线程池任务时候的threadlocal值,而是“残留”了上一次使用记录的方法调用信息

pool-1-thread-1 method call chain[ main-->queryShop-->record ]
pool-1-thread-2 method call chain[ main-->queryItem-->record ]
pool-1-thread-1 method call chain[ main-->queryShop-->recordqueryCoupon-->record ]

4.5 结论

我们需要一种机制把 任务提交给线程池时的ThreadLocal值传递到 任务执行时。

5. 使用TransmittableThreadLocal实现线程池复用的传值

5.1 修饰Runnable

  • 用法简介
    • 获取原生的实现runnable的task
    • 使用TtlRunnable修饰原生的runnable
  • 代码示例
package com.test.zihao;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TtlRunnableTest {
    private static final CountDownLatch latch = new CountDownLatch(2);
    public static void main(String[] args) throws InterruptedException {
        TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
        parent.set("value-set-in-parent");
        ConcurrentHashMap<String, TransmittableThreadLocal<String>> ttlInstance =
                new ConcurrentHashMap<String, TransmittableThreadLocal<String>>();
        ttlInstance.put("1",parent);
        Runnable task = new MyTask("thread1","1",ttlInstance);
        Runnable task1 = new MyTask("thread2","1",ttlInstance);
        Runnable ttlRunnable = TtlRunnable.get(task);
        Runnable ttlRunnable1 = TtlRunnable.get(task1);
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.submit(ttlRunnable);
        executorService.submit(ttlRunnable1);
        //等待子线程全部完成后 尝试从main线程获取对应的TransmittableThreadLocal
        latch.await();
        
        System.out.println("after task and task1 finished, parent : "+ttlInstance.get("1").get());
        executorService.shutdown();
    }
    static class MyTask implements Runnable{
        private ConcurrentHashMap<String, TransmittableThreadLocal<String>> ttlInstance;
        private String tag;
        public MyTask(String name,String tag, ConcurrentHashMap<String, TransmittableThreadLocal<String>> ttlInstance){
            Thread.currentThread().setName(name);
            this.tag = tag;
            this.ttlInstance = ttlInstance;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+" child before set: "+ttlInstance.get("1").get());
            ttlInstance.get("1").set("value-set-in-child"+new Random().nextInt(10));
            System.out.println(Thread.currentThread().getName()+" child after set: "+ttlInstance.get("1").get());
            latch.countDown();
        }
    }
}
  • 代码输出
pool-1-thread-1 child before set: value-set-in-parent
pool-1-thread-2 child before set: value-set-in-parent
pool-1-thread-1 child after set: value-set-in-child4
pool-1-thread-2 child after set: value-set-in-child1
after task and task1 finished, parent : value-set-in-parent

5.2 修饰Callable

  • 用法简介

    • 获取原生的实现callable的task
    • 使用TtlCallable修饰原生的callable
  • 代码示例

package com.test.zihao;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlCallable;
import java.util.Random;
import java.util.concurrent.*;
/**
 * @author tangzihao
 * @date 2020/9/6 9:34 下午
 */
public class TtlCallableTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
        parent.set("value-set-in-parent");
        ConcurrentHashMap<String, TransmittableThreadLocal<String>> ttlInstance =
                new ConcurrentHashMap<String, TransmittableThreadLocal<String>>();
        ttlInstance.put("1", parent);
        Callable<String> task = new MyTask("thread1", "1", ttlInstance);
        Callable<String> task1 = new MyTask("thread2", "1", ttlInstance);
        Callable<String> ttlCallable = TtlCallable.get(task);
        Callable<String> ttlCallable1 = TtlCallable.get(task1);
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future<String> future = executorService.submit(ttlCallable);
        Future<String> future1 = executorService.submit(ttlCallable1);
        System.out.println("thread1 after set: " + future.get());
        System.out.println("thread2 after set: " + future1.get());
        System.out.println("after task and task1 finished, parent : " + ttlInstance.get("1").get());
        executorService.shutdown();
    }
    static class MyTask implements Callable<String> {
        private ConcurrentHashMap<String, TransmittableThreadLocal<String>> ttlInstance;
        private String tag;
        public MyTask(String name, String tag, ConcurrentHashMap<String, TransmittableThreadLocal<String>> ttlInstance) {
            Thread.currentThread().setName(name);
            this.tag = tag;
            this.ttlInstance = ttlInstance;
        }
        @Override
        public String call() throws Exception {
            System.out.println(Thread.currentThread().getName() + " child before set: " + ttlInstance.get("1").get());
            ttlInstance.get("1").set("value-set-in-child" + new Random().nextInt(10));
            return ttlInstance.get("1").get();
        }
    }
}
  • 代码输出
pool-1-thread-1 child before set: value-set-in-parent
pool-1-thread-2 child before set: value-set-in-parent
thread1 after set: value-set-in-child9
thread2 after set: value-set-in-child4
after task and task1 finished, parent : value-set-in-parent

5.3 修饰线程池

  • 使用方法
    • 使用原生的ExecutorService
    • 使用TtlExecutors包装ExecutorService
  • 代码示例
package com.test.zihao;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 调用链
 *        |----queryShop----record
 * main---|----queryItem----record
 *        |----queryCoupon---record
 * @author tangzihao
 * @date 2020/9/5 11:17 下午
 */
public class TtlExecutorTest {
    private static final TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();
    private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
    public static void main(String[] args) {
        threadLocal.set("main-->");
        executorService.submit(TtlExecutorTest::queryShop);
        executorService.submit(TtlExecutorTest::queryItem);
        executorService.submit(TtlExecutorTest::queryCoupon);
    }
    /**
     * 查询店铺信息
     */
    private static void queryShop() {
        threadLocal.set(threadLocal.get() + "queryShop");
        record();
    }
    /**
     * 查询商品
     */
    private static void queryItem() {
        threadLocal.set(threadLocal.get() + "queryItem");
        record();
    }
    /**
     * 查询优惠券
     */
    private static void queryCoupon() {
        threadLocal.set(threadLocal.get() + "queryCoupon");
        record();
    }
    /**
     * 记录日志
     */
    private static void record() {
        threadLocal.set(threadLocal.get() + "-->record");
        System.out.println(Thread.currentThread().getName() + " method call chain[ " + threadLocal.get() + " ]");
    }
}
  • 输出
pool-1-thread-1 method call chain[ main-->queryShop-->record ]
pool-1-thread-2 method call chain[ main-->queryItem-->record ]
pool-1-thread-1 method call chain[ main-->queryCoupon-->record ]

*其他

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