揭开神秘面纱——深入浅出ThreadLocal
姓名:谢艾芳 学号: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()方法进行清除,避免造成内存泄露。