嵌牛IT观察

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

2017-12-14  本文已影响0人  天使和双彩虹2

姓名:谢艾芳  学号: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()方法进行清除,避免造成内存泄露。

上一篇下一篇

猜你喜欢

热点阅读