美文网首页java 并发
套路之ThreadLocal

套路之ThreadLocal

作者: 一帅 | 来源:发表于2017-05-23 18:12 被阅读154次

    理解ThreadLocal

    我们先来讨论一下打架策略的问题,然后再来看看什么是ThreadLocal吧。如果你遇到三个歹徒,你会怎么办呢? 假设你是个练家子,我想最容易想到的就是策略:以一敌三。为了不受伤害,你是辗转挪腾,左躲右闪,尽量保证同一时刻可以只对付一名歹徒,以便能够全身而退。如果把三名歹徒对你的攻击顺序比作三个线程的话,你实际上只在使用同步的方式来管理者三个线程对你的访问。使用同步方式来管理多个线程对同一个对象的访问是最常用的方式。但是,你也看到了,你对付得很辛苦,稍有不慎,就有可能受伤。但是如果你看过《火影忍者》的话,那么你还可以想到另外一种比较省事而且安全的策略:分身术。你可以变出三个分身来分别同时对付三个歹徒。这实际上是实现线程安全的另外一种策略:线程封闭。也就是说你啊就别折腾了,我给每一个线程都分配了资源了,就不需要去抢其他线程的资源了。其实这就是ThreadLocal的核心思想:通过避免线程间的共享来达到线程安全的目的。

    ThreadLocal的实现原理:实线代表强引用,虚线代表弱引用.

    每一个Thread对象都有一个ThreadLocal.ThreadLocalMap类型的名为ThreadLocals的实例变量,它就是保持那些通过 ThreadLocal设置给这个线程的数据资源的地方。当通过ThreadLocal的set(data)方法来设置数据的时候,ThreadLocal会首先获取当前线程的引用,然后通过该应用获取当前线程持有的threadLocals,最后以ThreadLocal最为Key,将要设置的数据设置到当前线程,如下

        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    

    实际上,ThreadLocal就像是一个窗口,通过这个窗口,我们可以将特定于线程的数据资源绑定到当前线程,也可以通过这个窗口获取绑定的数据资源,当然也可以解除之前绑定到线程的数据资源。在整个线程的生命周期内,我们都可以通过ThreadLocal这个窗口来与当前线程打交道。

    为了更好地理解Thread和ThreadLocal之间的关系的,我们不妨假设城市的公交系统。城市中的各条公交线路就好像我们系统中的每一个线程,在每条公交线路上,会有相应的公交车辆,这些公交车辆就好像是Thread中的thredLocals,用来运输特定于该条线路的乘客(数据资源),为了乘客可以上车或者下车,各条公交线路在沿路上都设置了多个乘车点,而这些乘车点实际上就是ThreadLocal。虽然同一个乘车点可能会有多条线路公用,单在同一时间,乘车只会搭乘他要乘坐并且当前经过的公交车。这与ThreadLocal和Thread的关系是相似的,虽然同一个ThreadLocal可以为多个线程指定数据资源,但只会将数据绑定到当前线程。


    ThreadLocal可能引起的内存泄露

    threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用threadlocal的remove方法

    每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

    所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

    PS.Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。

    ThreadLocal的使用场景

    首先我们可以从两个方面来看待并使用ThreadLocal

    • 横向上看,我们更看重于ThreadLocal跨越多个线程的能力。为了以更加简单的方式来管理应用程序的线程安全,ThreadLocal干脆将没有必要共享的对象不共享,直接为每一个线程分配一份各自特定的数据资源。
    • 纵向上看,我们更侧重于ThreadLocal能够将数据资源绑定到当前线程的能力。这样我们通过ThreadLocal设置的特定于各个线程的数据资源,可以随着所在线程的执行流程“随波逐流”。

    当然,这两个方面不是相互独立的,更多时候是相互依存,紧密结合的。在充分发挥ThreadLocal两方面的能力的基础上,我们可以总结出ThreadLocal的以下应用场景。

    • 管理应用程序的线程安全
      对于某些有状态或者非线程安全的对象,我们可以在多线程程序中为每一个线程分配对应的副本,而不是让多个线程共享该类型的对象,而从避免了需要协调多个线程对这些对象进行访问的“危险”的工作。数据库连接就是这一类对象。所以我们可以为每一个线程分配一个独立的Connection对象,从而避免了单一Connection对象的争用。而且,在JDBC中一个Connection就对应了一个事务,如果所有的线程都公用一个connection的话,那么整个事务管理就失控了。
    • 线程内的数据传递
      采用ThreadLocal来进行当前流程的参数传递,可以避免耦合性很强的方法参数形式的传递方式。但这有些像是让数据随着“暗流”漂泊的意思。一旦处理不当就会出现“触礁”之类的事故。比如,资源没有进行合理的清理导致心痛行为异常。所以通常应该通过一组框架来规范并屏蔽对ThreadLocal的直接操作,尽量避免应用代码的直接接触
    • 某些情况下的性能优化

    具体的使用场景可以在我下一篇博文中查看,这一篇暂时就到这。

    相关文章

      网友评论

        本文标题:套路之ThreadLocal

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