美文网首页
ThreadLocal、ITL、TTL原理详解及实践

ThreadLocal、ITL、TTL原理详解及实践

作者: Raral | 来源:发表于2023-01-08 17:20 被阅读0次

    1.ThreadLocal 介绍
    1.1基本使用
    1.2原理分析
    1.3软引用
    2.InheritableThreadLocal 介绍
    2.1基本使用
    2.2原理分析
    2.3ITL问题

    3.TransmittableThreadLocal 介绍
    3.1基本使用
    3.2原理分析
    3.3ITL问题

    <h1 id='1'>一、ThreadLocal(TL)</h1>
    项目中我们如果想要某个对象在程序运行中的任意位置获取到,就需要借助ThreadLocal来实现,这个对象称作线程的本地变量,下面就介绍下ThreadLocal是如何做到线程内本地变量传递的,

    <h2 id='1.1'>1.1基本使用</h2>

    //当前线程上下文
        public static final ThreadLocal threadLocal = new ThreadLocal();
    
        public static void main(String[] args) throws Exception {
            //登录
            LoginHandle();
            System.out.println(String.format("当前线程名称: %s, 线程内数据为: %s",
                    Thread.currentThread().getName(), threadLocal.get()));
            //查询
            new Thread(() -> {
                String info = getInfo();
                System.out.println(String.format("当前线程名称: %s, 获取线程内数据为: %s",
                        Thread.currentThread().getName(), info));
    
                //在子线程设置上下文,看是否影响主线程 上下文 值
                threadLocal.set("ok2");
    
            }).start();
    
            //确保下面执行在上面的异步代码之后执行
            Thread.sleep(1000);
            System.out.println(String.format("当前线程名称: %s, 线程内数据为: %s",
                    Thread.currentThread().getName(), threadLocal.get()));
    
    
    
        }
    
        public static void LoginHandle() {
            threadLocal.set("ok");
        }
    
        public static String getInfo() {
            String o =(String) threadLocal.get();
            threadLocal.remove();
            return o;
        }
    
    
    /**打印
    当前线程名称: main, 线程内数据为: ok
    当前线程名称: Thread-0, 获取线程内数据为: null
    当前线程名称: main, 线程内数据为: ok
    
    */
    

    通过上面代码分析

    1. ThreadLocal 在多线程中是线程数据隔离的,线程之间不能访问彼此上下文的。
    2. ThreadLocal 数据共享只能在当前线程 操作数栈中。

    <h2 id='1.2'>1.2原理分析</h2>
    敬请期待

    <h1 id='2'>二、InheritableThreadLocal</h1>
    根据ThreadLocal(TL)特点,父线程的本地变量无法传递给子线程;InheritableThreadLocal(ITL)为了解决这个问题,可以实现 父线程的本地变量可以传递给子线程

    <h2 id='2.1'>2.1基本使用</h2>

    只修改下InheritableThreadLocal

    //当前线程上下文
        public static final InheritableThreadLocal threadLocal = new InheritableThreadLocal();
    
        public static void main(String[] args) throws Exception {
            //登录
            LoginHandle();
            System.out.println(String.format("当前线程名称: %s, 线程内数据为: %s",
                    Thread.currentThread().getName(), threadLocal.get()));
            //查询
            new Thread(() -> {
                String info = getInfo();
                System.out.println(String.format("当前线程名称: %s, 获取线程内数据为: %s",
                        Thread.currentThread().getName(), info));
    
                //在子线程设置上下文,看是否影响主线程 上下文 值
                threadLocal.set("ok2");
    
            }).start();
    
            //确保下面执行在上面的异步代码之后执行
            Thread.sleep(1000);
            System.out.println(String.format("当前线程名称: %s, 线程内数据为: %s",
                    Thread.currentThread().getName(), threadLocal.get()));
    
    
    
        }
    
        public static void LoginHandle() {
            threadLocal.set("ok");
        }
    
        public static String getInfo() {
            String o =(String) threadLocal.get();
            threadLocal.remove();
            return o;
        }
    
    
    /**打印
    当前线程名称: main, 线程内数据为: ok
    当前线程名称: Thread-0, 获取线程内数据为: ok
    当前线程名称: main, 线程内数据为: ok
    */  
    

    <h2 id='2.2'>2.2原理分析</h2>
    敬请期待
    <h2 id='2.3'>2.3ITL问题</h2>

    1. 线程不安全
      如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量(本质上是同一个对象),参考上面的第三个例子,子线程写入后会覆盖掉主线程的变量,也是通过这个结果,我们确认了子线程TLMap里变量指向的对象和父线程是同一个。
    2. 线程池中可能失效
      按照上述实现,在使用线程池的时候,ITL会完全失效,因为父线程的TLMap是通过init一个Thread的时候进行赋值给子线程的,而线程池在执行异步任务时可能不再需要创建新的线程了,因此也就不会再传递父线程的TLMap给子线程了。
     private static ExecutorService executorService = Executors.newFixedThreadPool(1);
    
        public static final InheritableThreadLocal<String> itl = new InheritableThreadLocal();
    
        public static void main(String[] args) {
    
    
            System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), itl.get()));
    
            executorService.execute(() -> {
                System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), itl.get()));
            });
            itl.set("ok");//等上面的线程池第一次用完,父线程再进行赋值
    
            executorService.execute(() -> {
                System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), itl.get()));
            });
            System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), itl.get()));
    
    
    
        }
    
       /**
       
    线程名称-main, 变量值=null
    线程名称-pool-1-thread-1, 变量值=null
    线程名称-pool-1-thread-1, 变量值=null
    线程名称-main, 变量值=ok
       */ 
    

    解决

    但是,在实际项目中我们大多数采用线程池进行做异步任务,假如真的需要传递主线程的本地变量,使用ITL的问题显然是很大的,因为是有极大可能性拿不到任何值的,显然在实际项目中,ITL的位置实在是尴尬,所以在启用线程池的情况下,不建议使用ITL做值传递。为了解决这种问题,阿里做了transmittable-thread-local(TTL)来解决线程池异步值传递问题,下一篇,我们将会分析TTL的用法及原理。

    <h1 id='3'>一、TransmittableThreadLocal(TTL)</h1>
    首先,TTL是用来解决ITL解决不了的问题而诞生的,所以TTL一定是支持父线程的本地变量传递给子线程这种基本操作的,ITL也可以做到,但是前面有讲过,ITL在线程池的模式下,就没办法再正确传递了,所以TTL做出的改进就是即便是在线程池模式下,也可以很好的将父线程本地变量传递下去,
    <h2 id='3.1'>2.2基本使用</h2>

    private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
    
        //当前线程上下文
        public static final TransmittableThreadLocal ttl = new TransmittableThreadLocal<>();
    
        public static void main(String[] args) throws Exception {
            System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), ttl.get()));
    
            executorService.execute(() -> {
                System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), ttl.get()));
            });
            ttl.set("ok");//等上面的线程池第一次用完,父线程再进行赋值
    
            executorService.execute(() -> {
                System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), ttl.get()));
            });
            System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), ttl.get()));
    
        }
    
    /**
    
    线程名称-main, 变量值=null
    线程名称-pool-1-thread-1, 变量值=null
    线程名称-main, 变量值=ok
    线程名称-pool-1-thread-2, 变量值=ok
    */    
    
    private static ThreadLocal tl = new TransmittableThreadLocal<>();
        public static void main(String[] args) {
            new Thread(() -> {
    
                String mainThreadName = "main_01";
    
                tl.set(1);
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                sleep(1L); //确保上面的会在tl.set执行之前执行
                tl.set(2); // 等上面的线程池第一次启用完了,父线程再给自己赋值
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
    
            }).start();
    
    
            new Thread(() -> {
    
                String mainThreadName = "main_02";
    
                tl.set(3);
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                sleep(1L); //确保上面的会在tl.set执行之前执行
                tl.set(4); // 等上面的线程池第一次启用完了,父线程再给自己赋值
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                new Thread(() -> {
                    sleep(1L);
                    System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
                }).start();
    
                System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
    
            }).start();
        }
    
        private static void sleep(long time) {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    

    <h2 id='3.1'>3.2原理分析</h2>
    敬请期待

    <h2 id='3.2'>3.3总结</h2>
    到这里基本上确认了TTL是如何进行线程池传值的,以及被包装的run方法执行异步任务之前,会使用replay进行设置父线程里的本地变量给当前子线程,任务执行完毕,会调用restore恢复该子线程原生的本地变量(目前原生本地变量的产生,就只碰到上述测试代码中的这一种情况,即线程第一次使用时通过ITL属性以及Thread的init方法传给子线程,还不太清楚有没有其他方式设置)。

    其实,正常程序里想要完成线程池上下文传递,使用TL就足够了,我们可以效仿TTL包装线程池对象的原理,进行值传递,异步任务结束后,再remove,以此类推来完成线程池值传递,不过这种方式过于单纯,且要求上下文为只读对象,否则子线程存在写操作,就会发生上下文污染。

    TTL项目地址(可以详细了解下它的其他特性和用法):https://github.com/alibaba/transmittable-thread-local
    https://www.cnblogs.com/hama1993/p/10409740.html

    相关文章

      网友评论

          本文标题:ThreadLocal、ITL、TTL原理详解及实践

          本文链接:https://www.haomeiwen.com/subject/knvucdtx.html