为什么需要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 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 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 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 key = (ThreadLocal) 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 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 parent = new TransmittableThreadLocal();
        parent.set("value-set-in-parent");
        ConcurrentHashMap> ttlInstance =
                new ConcurrentHashMap>();
        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> ttlInstance;
        private String tag;
        public MyTask(String name,String tag, ConcurrentHashMap> 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 parent = new TransmittableThreadLocal();
        parent.set("value-set-in-parent");
        ConcurrentHashMap> ttlInstance =
                new ConcurrentHashMap>();
        ttlInstance.put("1", parent);
        Callable task = new MyTask("thread1", "1", ttlInstance);
        Callable task1 = new MyTask("thread2", "1", ttlInstance);
        Callable ttlCallable = TtlCallable.get(task);
        Callable ttlCallable1 = TtlCallable.get(task1);
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future future = executorService.submit(ttlCallable);
        Future 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 {
        private ConcurrentHashMap> ttlInstance;
        private String tag;
        public MyTask(String name, String tag, ConcurrentHashMap> 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 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);

你可能感兴趣的:(为什么需要TransmittableThreadLocal)