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

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

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

推荐阅读更多精彩内容