工作面试中经常遇到ThreadLocal,但是很多同学并不了解ThreadLocal实现原理,到底为什么会发生内存泄漏也是一知半解?今天一灯带你深入剖析ThreadLocal源码,总结ThreadLocal使用规范,解析ThreadLocal高频面试题。,ThreadLocal是线程本地变量,就是线程的私有变量,不同线程之间相互隔离,无法共享,相当于每个线程拷贝了一份变量的副本。,目的就是在多线程环境中,无需加锁,也能保证数据的安全性。,ThreadLocal的用法非常简单,创建ThreadLocal的时候指定泛型类型,然后就是赋值、取值、删除值的操作。,不同线程之间,ThreadLocal数据是隔离的,测试一下:,输出结果:,可以看出不同线程之间的ThreadLocal数据相互隔离,互不影响,这样的实现效果有哪些应用场景呢?,ThreadLocal的应用场景主要分为两类:,避免对象在方法之间层层传递,打破层次间约束。比如用户信息,在很多地方都需要用到,层层往下传递,比较麻烦。这时候就可以把用户信息放到ThreadLocal中,需要的地方可以直接使用。,拷贝对象副本,减少初始化操作,并保证数据安全。比如数据库连接、Spring事务管理、SimpleDataFormat格式化日期,都是使用的ThreadLocal,即避免每个线程都初始化一个对象,又保证了多线程下的数据安全。,使用ThreadLocal保证SimpleDataFormat格式化日期的线程安全,代码类似下面这样:,ThreadLocal底层使用ThreadLocalMap存储数据,而ThreadLocalMap内部是一个数组,数组里面存储的是Entry对象,Entry对象里面使用key-value存储数据,key是ThreadLocal实例对象本身,value是ThreadLocal的泛型对象值。,
,再看一下实际的set方法源码:,set方法具体流程如下:,
,从源码和流程图中得知,ThreadLocal是通过线性探测法解决哈希冲突的,线性探测法具体赋值流程如下:,通过key的hashcode找到数组下标,如果数组下标位置是空或者等于当前ThreadLocal对象,直接覆盖值结束,如果不是空,就继续向下遍历,遍历到数组结尾后,再从头开始遍历,直到找到数组为空的位置,在此位置赋值结束,线性探测法这种特殊的赋值流程,导致取值的时候,也要走一遍类似的流程。,再看一下具体的遍历Entry数组的逻辑:,再看一下线性探测法特殊的取值方法:,ThreadLocal的get方法流程如下:,
,remove方法流程跟set、get方法类似,都是遍历数组,找到ThreadLocal实例对象后,删除key、value,再删除Entry对象结束。,使用ThreadLocal结束,一定要调用remove方法,清理掉threadLocal数据。具体流程类似下面这样:,如果忘了调用remove方法,可能会导致两个严重的问题:,导致内存溢出如果线程的生命周期很长,一直往ThreadLocal中放数据,却没有删除,最终产生OOM,导致数据错乱如果使用了线程池,一个线程执行完任务后并不会被销毁,会继续执行下一个任务,导致下个任务访问到了上个任务的数据。,看完了ThreadLocal源码,再回答几道面试题,检验一下学习成果怎么样。,ThreadLocal底层使用的ThreadLocalMap存储数据,而ThreadLocalMap是线程Thread的私有变量,不同线程之间数据隔离,所以即使ThreadLocal的set、get、remove方法没有加锁,也能保证线程安全。,
,因为在一个线程中可以创建多个ThreadLocal实例对象,所以要用数组存储,而不是用一个对象。,ThreadLocal使用的线性探测法法解决哈希冲突,线性探测法法具体赋值流程如下:,通过key的hashcode找到数组下标,如果数组下标位置是空或者等于当前ThreadLocal对象,直接覆盖值结束,如果不是空,就继续向下遍历,遍历到数组结尾后,再从头开始遍历,直到找到数组为空的位置,在此位置赋值结束,我们都知道HashMap采用的是链地址法(也叫拉链法)解决哈希冲突,为什么ThreadLocal要用线性探测法解决哈希冲突?而不用链地址法呢?,我的猜想是可能是创作者偷懒、嫌麻烦,或者是ThreadLocal使用量较少,出现哈希冲突概率较低,不想那么麻烦。,使用链地址法需要引入链表和红黑树两种数据结构,实现更复杂。而线性探测法没有引入任何额外的数据结构,直接不断遍历数组。,结果就是,如果一个线程中使用很多个ThreadLocal,发生哈希冲突后,ThreadLocal的get、set性能急剧下降。,线性探测法相比链地址法优缺点都很明显:,优点: 实现简单,无需引入额外的数据结构。,缺点: 发生哈希冲突后,ThreadLocal的get、set性能急剧下降。,先说一下弱引用的特点:,弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。,ThreadLocalMap的key设计成弱引用后,会不会我们正在使用,就被GC回收了?,这个是不会的,因为我们一直在强引用着ThreadLocal实例对象。,由上面代码中得知,如果我们一直在使用threadLocal,触发GC后,并不会threadLocal实例对象。,ThreadLocalMap的key设计成弱引用的目的就是:,防止我们在使用完ThreadLocal后,忘了调用remove方法删除数据,导致数组中ThreadLocal数据一直不被回收。,ThreadLocal出现内存泄漏的原因,就是我们使用完ThreadLocal没有执行remove方法删除数据。,具体是哪些数据过多导致的内存泄漏呢?,一个是数组的Entry对象,Entry对象中key、value分别是ThreadLocal实例对象和泛型对象值。,因为我们在使用ThreadLocal的时候,总爱把ThreadLocal设置成类的静态变量,直到线程生命周期结束,ThreadLocal对象数据才会被回收。,另一个是数组中Entry对象的value值,也就是泛型对象值。虽然ThreadLocalMap的key被设置成弱引用,会被GC回收,但是value并没有被回收。需要等到下次执行get、set方法遍历数组,遍历到这个位置,才会删除这个无效的value。这也是造成内存泄漏的原因之一。,只需要InheritableThreadLocal即可,当初始化子线程的时候,会从父线程拷贝ThreadLocal数据。,
© 版权声明
文章版权归作者所有,未经允许请勿转载。