美文网首页嵌牛IT观察
揭开神秘面纱——深入浅出ThreadLocal

揭开神秘面纱——深入浅出ThreadLocal

作者: 天使和双彩虹2 | 来源:发表于2017-12-14 20:56 被阅读0次

    姓名:谢艾芳  学号:16040410073

    转自http://www.jianshu.com/p/312019513c48

    〖嵌牛导读〗能够找到这篇文章,说明你已开始学习Java的多线程了,也了解多线程的同步、锁等概念。但,ThreadLocal虽出现在多线程的环境中,对于它的使用,并不涉及到锁和同步的概念。它生于多线程,伴随着多线程的热点,而并不沾染多线程的常见问题,是不是莫名的小清新呢?带着好奇和疑惑,一起深入ThreadLocal吧!

    〖嵌牛鼻子〗ThreadLocal  多线程

    〖嵌牛提问〗如果你对ThreadLocal有所了解,听说过内存泄露,如何才能更好的驾驭它呢?

    〖嵌牛正文〗

    1.背景随便举两个具体的例子:

    1)在一个web项目中,从请求一进来就为之生成一个 uuid,无论系统是否报异常,返回给客户的必须是同一 uuid,可能首先会想到当作方法的参数来传递,这样任何地方可以成功的获取到这个 uuid,但这个 uuid 会在系统中几乎各个方法参数中都会出现,但 uuid 又非主要业务参数,这样势必会与业务耦合性太强。2.对于很多非线程安全的类而言,如工具类:SimpleDateFormat和JDBC的Connection,它们经常出现在并发环境中,例如Connection,大家刚接触JDBC的时候,都是在方法中完成Connection的 init/commit/close,如多个线程都想连接数据库执行sql,方法有如下:(1)对Connection进行同步加锁,协调各个线程操作DB的顺序,没错但很低效。(2)每个线程自己创建Connection,会造成频繁的创建和释放连接,线程结束,Connection也就结束。

    2. 与并发/同步的区别

    什么,你突然想到了并发中的同步?先立一个flag,其实他们有本质的区别,同步是协调多个线程对同一个变量的修改,而ThreadLocal则是将这个变量的副本据为线程己有,各个线程操作的是各自的threadlocal变量,各个线程互不影响,自然不会涉及到同步。

    3. 易混名词释疑

    大家容易搞混Thread/ThreadLocal/ThreadLocalMap三者的关系,其实很简单,如图:

    关系图 4. 源码时间

    作为专业的看官,等的就是代码,静下心来,15min后,让你感受到咸鱼翻身,虽然还是咸鱼,哈哈

    来看ThreadLocal这个类,其中包括 get/set/remove 等方法,为了避免码字嫌疑,只贴关键代码(其中加入了笔者Norman的中文注释,帮助理解),下面逐个介绍:

    4.1 set()

    代码包含set()方法,同时包括方法体内所调用的其他方法(后同)


    揭开神秘面纱——深入浅出ThreadLocal
    揭开神秘面纱——深入浅出ThreadLocal
    揭开神秘面纱——深入浅出ThreadLocal
    整体流程可以看出,当调用set(T value)方法时,会先取出本线程的ThreadLocalMap,对于Map:

    ·如果不为空,则以ThreadLocal实例为key, 将value存储在此Map中

    ·如果为空,就创建一个Map,并将其赋值给此线程的成员变量threadLocals

    对于ThreadLocalMap是由谁来维护,其定义的代码如下:

    结合代码段2,可以看出,ThreadLocalMap其实是定义在ThreadLocal中的静态内部类,然后由Thread类来维护,依附于Thread的生命周期。读过HashMap源码的童鞋知道Entry是什么东东,这个Entry 继承了 WeakReference类,其实就Entry的key继承了它,从构造函数就可以看出,顺带简单回顾下java 的引用:

    ·强引用:不受GC影响,即时OOM也不回收;eg. Person p = new Person("Norman")

    ·软引用:只会在内存不足时,由GC回收;

    ·弱引用:不论内存是否够用,一旦GC,则回收,不过GC的线程优先级,不一定很快的发现;

    ·虚引用:形同虚设,与前三不同,它必须配合WeakReferenceQueue,跟踪对象被垃圾回收的活动

    那么,为什么要用到弱引用呢?官方文档如是说:

    如下场景很好的解释了这样设计的好处(感谢xiaohansong):

    强引用: 当对象A中引用ThreadLocal的对象B,A被回收,则B变为垃圾,但线程对Map是强引用,Map对B是 强 引用,只要线程存活,则B始终不会被回收。

    弱引用: 当对象A中引用ThreadLocal的对象B,A被回收,则B变为垃圾,由于线程对Map是强引用,Map对B是 弱 引用,即使没有手动删除,在下一个GC周期,B也会被回收掉。而Map中的value会在调用set/get/remove方法后断掉强引用,等待GC后续回收(见 4.4 内存泄露)。

    4.2 get()



    如果,当前线程没有threadLocal值,则默认调用initialValue()方法,其中的取值可以看到,ThreadLocalMap中处理Hash冲突的方法是线性探测法,顺带回顾下数据结构中,Hash冲突的解决办法:

    1.开放地址法

    o线性探测 (ThreadLocalMap)

    o二次探测

    o再哈希

    2.再哈希法

    3.链地址法 (HashMap)

    4.建立一个公共溢出区

    4.3 remove()

    揭开神秘面纱——深入浅出ThreadLocal
    揭开神秘面纱——深入浅出ThreadLocal
    揭开神秘面纱——深入浅出ThreadLocal
    揭开神秘面纱——深入浅出ThreadLocal

    4.4 内存泄露

    从源码中看,无论是get(),set()还是remove()操作,都会包含对ThreadLocalMap 中key为null的Entry清除,那么泄露会出现在什么地方呢?仔细来看各部分依赖图:

    内部关联

    ThreadLocal可手动置为null,也可以由GC置null(因为弱引用),但这只是针对key,对于value,当前Entry的value被Entry引用,而Entry被当前Map引用,而Map则被当前线程实例Thread引用,如果当前线程不退出,则value是不会被GC,造成内存泄露。更加准确的说,是发生在 :当Map中的key(ThreadLocal)为null后到线程结束 这期间。当遇到线程池,线程会被重复利用,如果使用 set 后不再使用 get/set/remove,这个强应用会一直存在,造成内存泄露。(PS:当value是大对象时尤为严重)

    那补救措施有哪些呢?

    (1)首先,jdk本身get/set/remove操作会清除key为null的Entry,但属于被动清除,不调用此方法,依然会内存泄露

    (2)其次,当用完threadLocal后,应该主动调用remove方法,主动断掉value到thread的引用链

    5. 总结使用ThreadLocal有一些建议:

    (1)使用static修饰,使之属于类而不是实例,因为它持有的对象,生效范围一般在用户会话/web请求周期期间。

    揭开神秘面纱——深入浅出ThreadLocal

    (2)如上文提到,使用结束后调用remove()方法进行清除,避免造成内存泄露。

    相关文章

      网友评论

        本文标题:揭开神秘面纱——深入浅出ThreadLocal

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