ThreadLocal的介绍及由其引发的内存泄露问题
一、ThreadLocal的定义
ThreadLocal,通常被我们翻译为线程本地变量,这是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
二、ThreadLocal的源码解析及常用方法
先来看一下ThreadLocal的源码:
可以看出,ThreadLocal的实现是通过维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
查看一下ThreadLocalMap的源码:
可以看出,是通过继承弱引用的Entry键值对来实现的,也就是存储ThreadLocal本身的键是弱引用,此设计为ThreadLocal所引发的内存泄露埋下了伏笔,后面再谈,先来说一下ThreadLocal中常用的方法:set(T value)、get()、remove()。
通过set(T value)可以向当前本地变量设置一个值:
通过get()方法可以获取到这个值:
而remove()就是把当前的这个值给设置为null。
三、ThreadLocal的使用
ThreadLocal在实际中该怎么用呢?
1、由于ThreadLocal是线程本地变量,所有我们可以获取处于两个方法的调用的同一线程的执行耗时,比如在AOP(面向方面编程)中,可以在方法调用前的切入点执行begin()方法,而在方法调用后的切入点执行end()方法,这样通过ThreadLocal的set()和get()方法依旧可以获得执行耗时。
2、维持线程封闭性。使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
3、ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。从概念上看,你可以将ThreadLocal<T>视为包含了Map< Thead,T> 对象,其中保存了特定于该线程的值,但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收。假设你需要将一个单线程应用程序移植到多线程环境中, 通过将共享的全局变量转换为Theadl ocal对象,可以维持线程安全性。
四、ThreadLocal使用不当造成的内存泄露
导致ThreadLocal使用不当造成的内存泄露的关键在于ThreadLocalMap<ThreadLocal, Object>弱引用问题:当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。
在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap。Map中的key为一个ThreadLocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向ThreadLocal,当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被gc回收。但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。只有当前线程结束以后,current thread就不会存在栈中,强引用断开, value将全部被GC回收。
所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在ThreadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。不过最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
五、怎么规避由ThreadLocal引起的内存泄露
通过以上分析,从表象上来看,ThreadLocalMap<ThreadLocal, Object>弱引用问题造成了内存泄露。那么使用了强引用就可以规避这个问题了吗?我们从两方面来分析:
1、key使用强引用:当引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,在线程结束之前或者使用的是线程池,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
2、key使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
以上两种情况可以看出,重要的是我们没有手动删除。由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,使用弱引用可以多一层保障。
因此,ThreadLocal内存泄漏的根本原因是没有手动删除对应key,而不是因为弱引用。所有我们每次在使用完ThreadLocal后,都调用它的remove()方法,清除数据。
最后补充一句:ThreadLocal是用于线程间的数据隔离,是空间换时间的做法;而Synchronized用于线程间的数据共享,两者有着本质的不同。