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

2023-01-08  本文已影响0人  Raral

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

上一篇下一篇

猜你喜欢

热点阅读