[TOC]
1. ThreadLocal 简介
网上看到一些文章, 提到关于 ThreadLocal 可能引起的内存泄露, 搞得都不敢在代码里随意使用了, 于是来研究下, 看看到底 ThreadLocal 会不会导致内存泄露, 什么情况下会导致泄露.
ThreadLocal, 顾名思义, 其存储的内容是线程私本地的 / 私有的, 我们常使用 ThreadLocal 来存储 / 维护一些资源或者变量, 以避免线程争用或者同步问题, 例如使用 ThreadLocal 来为每个线程维持一个 Redis 连接 (生产中这也许不是一个好的方式, 还是推荐专业的连接池) 或者维持一些线程私有的变量等.
例如, 假设我们在一个线程应用中需要对时间做格式化, 我们很容易想到的是使用 SimpleDateFormat 这个工具类, 但是 SimpleDateFormat 不是线程安全的, 那么我们通常用两种做法:
每次用到的时候 new 一个 SimpleDateFormat 对象, 使用完丢弃, 交给 gc
每个线程维护一个 SimpleDateFormat 实例, 线程运行期间不重复创建
那么无论从执行效率还是内存占用方面, 我们都倾向于使用后者, 即线程私有一个 SimpleDateFormat 对象, 这时候, ThreadLocal 就是很好的应用, 示例代码如下:
- import java.text.SimpleDateFormat;
- import java.util.Date;
- public class TestTask implements Runnable {
- private boolean stop = false;
- private ThreadLocal<SimpleDateFormat> sdfHolder = new ThreadLocal<SimpleDateFormat>() {
- @Override
- protected SimpleDateFormat initialValue() {
- return new SimpleDateFormat("yyyyMMdd");
- }
- };
- @Override
- public void run() {
- while (stop) {
- String formatedDateStr = sdfHolder.get().format(new Date());
- System.out.println("formated date str:" + formatedDateStr);
- //may be sleep for a while to avoid high CPU cost
- }
- sdfHolder.remove();
- }
- //something else
- }
代码中模拟了一个需要反复执行的 Task, 其 run 方法中, while 条件除非 stop 是 true, 否则就一直运转下去. 在该示例中通过 ThreadLocal 为每个线程实例化了一个 SimpleDateFormat 对象, 当需要的时候, 通过 get()获取即可, 实现了每个线程全程只有一个 SimpleDateFormat 对象. 同时在 stop 为 true 时使用 ThreadLocal 的 remove 方法删除当前线程使用的 SimpleDateFormat 对象, 以便于垃圾回收.
仅演示 ThreadLocal 用法, 暂不讨论代码设计
2. ThreadLocal 内存模型
上面我们简单介绍了 ThreadLocal 的概念和使用, 下面看下 ThreadLocal 的内存模型.
2.1 ThreadLocal 内存模型
2.1.1 私有变量存储在哪里
在代码中, 我们使用 ThreadLocal 实例提供的 set/get 方法来存储 / 使用 value, 但 ThreadLocal 实例其实只是一个引用, 真正存储值的是一个 Map, 其 key 实 ThreadLocal 实例本身, value 是我们设置的值, 分布在堆区. 这个 Map 的类型是 ThreadL.ThreadLocalMap(ThreadLocalMap 是 ThreadLocal 的内部类), 其 key 的类型是 ThreadLocal,value 是 Object, 类定义如下:
- static class ThreadLocalMap {
- ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
- table = new Entry[INITIAL_CAPACITY];
- int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
- table[i] = new Entry(firstKey, firstValue);
- size = 1;
- setThreshold(INITIAL_CAPACITY);
- }
- static class Entry extends WeakReference<ThreadLocal> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal k, Object v) {
- super(k);
- value = v;
- }
- }
- }
那么当我们重写 init 或者调用 set/get 的时候, 内部的逻辑是怎样的呢, 按照上面的说法, 应该是将 value 存储到了 ThreadLocalMap 中, 或者从已有的 ThreadLocalMap 中获取 value, 我们来通过代码分析一下.
ThreadLocal.set(T value)
set 的逻辑比较简单, 就是获取当前线程的 ThreadLocalMap, 然后往 map 里添加 KV,K 是 this, 也就是当前 ThreadLocal 实例, V 是我们传入的 value.
- /**
- * Sets the current thread's copy of this thread-local variable
- * to the specified value. Most subclasses will have no need to
- * override this method, relying solely on the {@link #initialValue}
- * method to set the values of thread-locals.
- *
- * @param value the value to be stored in the current thread's copy of
- * this thread-local.
- */
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
其内部实现首先需要获取关联的 Map, 我们看下 getMap 和 createMap 的实现
- /**
- * Get the map associated with a ThreadLocal. Overridden in
- * InheritableThreadLocal.
- *
- * @param t the current thread
- * @return the map
- */
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- /**
- * Create the map associated with a ThreadLocal. Overridden in
- * InheritableThreadLocal.
- *
- * @param t the current thread
- * @param firstValue value for the initial entry of the map
- * @param map the map to store.
- */
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
可以看到, getMap 就是返回了当前 Thread 实例的 map(t.threadLocals),create 也是创建了 Thread 的 map(t.threadLocals), 也就是说对于一个 Thread 实例, ThreadLocalMap 是其内部的一个属性, 在需要的时候, 可以通过 ThreadLocal 创建或者获取, 然后存放相应的值. 我们看下 Thread 类的关键代码
- public class Thread implements Runnable {
- /* ThreadLocal values pertaining to this thread. This map is maintained
- * by the ThreadLocal class. */
- ThreadLocal.ThreadLocalMap threadLocals = null;
- // 省略了其他代码
- }
可以看到, Thread 中定义了属性 threadLocals, 但其初始化和使用的过程, 都是通过 ThreadLocal 这个类来执行的.
ThreadLocal.get()
get 是获取当前线程的对应的私有变量, 是我们之前 set 或者通过 initialValue 指定的变量, 其代码如下
- /**
- * Returns the value in the current thread's copy of this
- * thread-local variable. If the variable has no value for the
- * current thread, it is first initialized to the value returned
- * by an invocation of the {@link #initialValue} method.
- *
- * @return the current thread's value of this thread-local
- */
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null)
- return (T)e.value;
- }
- return setInitialValue();
- }
- /**
- * Variant of set() to establish initialValue. Used instead
- * of set() in case user has overridden the set() method.
- *
- * @return the initial value
- */
- private T setInitialValue() {
- T value = initialValue();
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- return value;
- }
可以看到, 其逻辑也比较简单清晰:
获取当前线程的 ThreadLocalMap 实例
如果不为空, 以当前 ThreadLocal 实例为 key 获取 value
如果 ThreadLocalMap 为空或者根据当前 ThreadLocal 实例获取的 value 为空, 则执行 setInitialValue()
setInitialValue()内部如下:
调用我们重写的 initialValue 得到一个 value
将 value 放入到当前线程对应的 ThreadLocalMap 中
如果 map 为空, 先实例化一个 map, 然后赋值 KV
关键设计小结
代码分析到这里, 其实对于 ThreadLocal 的内部主要设计以及其和 Thread 的关系比较清楚了:
每个线程, 是一个 Thread 实例, 其内部拥有一个名为 threadLocals 的实例成员, 其类型是 ThreadLocal.ThreadLocalMap
通过实例化 ThreadLocal 实例, 我们可以对当前运行的线程设置一些线程私有的变量, 通过调用 ThreadLocal 的 set 和 get 方法存取
ThreadLocal 本身并不是一个容器, 我们存取的 value 实际上存储在 ThreadLocalMap 中, ThreadLocal 只是作为 TheadLocalMap 的 key
每个线程实例都对应一个 TheadLocalMap 实例, 我们可以在同一个线程里实例化很多个 ThreadLocal 来存储很多种类型的值, 这些 ThreadLocal 实例分别作为 key, 对应各自的 value
当调用 ThreadLocal 的 set/get 进行赋值 / 取值操作时, 首先获取当前线程的 ThreadLocalMap 实例, 然后就像操作一个普通的 map 一样, 进行 put 和 get
当然, 这个 ThreadLocalMap 并不是一个普通的 Map(比如常用的 HashMap), 而是一个特殊的, key 为渃引用的 map, 这个我们后面再详谈
2.1.2 ThreadLocal 内存模型
通过上一节的分析, 其实我们已经很清楚 ThreadLocal 的相关设计了, 对数据存储的具体分布也会有个比较清晰的概念. 下面的图是网上找来的常见到的示意图, 我们可以通过该图对 ThreadLocal 的存储有个更加直接的印象.
TheadLocal 内存模型
我们知道 Thread 运行时, 其实例和相关内容使用的内存属于 Stack(栈)区, 而普通的对象是存储在 Heap(堆)区. 根据上图, 基本分析如下:
线程运行时, 我们定义的 TheadLocal 对象被初始化, 存储在 Heap, 同时线程运行的栈区保存了指向该实例的引用, 也就是途中的 ThreadLocalRef
当 ThreadLocal 的 set/get 被调用时, 虚拟机会根据当前线程的引用也就是 CurrentThreadRef 找到其对应在堆区的实例, 然后查看其对用的 TheadLocalMap 实例是否被创建, 如果没有, 则创建并初始化.
Map 实例化之后, 也就拿到了该 ThreadLocalMap 的句柄, 然后如果将当前 ThreadLocal 对象作为 key, 进行存取操作
途中的虚线, 表示 key 对 ThreadLocal 实例的引用是个弱引用
3. 插曲: 强引用 / 弱引用
java 中的引用分为四种, 按照引用强度不同个, 从强到弱依次为: 强引用, 软引用, 弱引用和虚引用, 如果不是专门做 jvm 研究, 对其概念很难清晰的定义, 我们大致可以理解为, 其引用的强度, 代表了对内存占用的能力大小, 具体体现在 GC 的时候, 会不会被回收, 什么时候被回收.
ThreadLocal 被用作 TheadLocalMap 的弱引用 key, 这种设计也是 ThreadLocal 被讨论内存泄露的热点问题, 因此有必要了解一下什么是弱引用.
3.1 强引用
强引用虽然在开发过程中并不怎么提及, 但是无处不在, 例如我们在一个对象中通过如下代码实例化一个 StringBuffer 对象
StringBuffer buffer = new StringBuffer();
我们知道 StringBuffer 的实例通常是被创建在堆中的, 而当前对象持有该 StringBuffer 对象的引用, 以便后续的访问, 这个引用, 就是一个强引用.
对 GC 知识比较熟悉的可以知道, HotSpot JVM 目前的垃圾回收算法一般默认是可达性算法, 即在每一轮 GC 的时候, 选定一些对象作为 GC ROOT, 然后以它们为根发散遍历, 遍历完成之后, 如果一个对象不被任何 GC ROOT 引用, 那么它就是不可达对象, 则在接下来的 GC 过程中很可能会被回收.
强引用最重要的就是它能够让引用变得强(Strong), 这就决定了它和垃圾回收器的交互. 具体来说, 如果一个对象通过一串强引用链接可到达(Strongly reachable), 它是不会被回收的. 如果你不想让你正在使用的对象被回收, 这就正是你所需要的.
3.2 软引用
软引用是用来描述一些还有用但是并非必须的对象. 对于软引用关联着的对象, 在系统将要发生内存溢出异常之前, 将会把这些对象列进回收返回之后进行第二次回收. 如果这次回收还没有足够的内存, 才会抛出内存溢出异常. JDK1.2 之后提供了 SoftReference 来实现软引用.
相对于强引用, 软引用在内存充足时可能不会被回收, 在内存不够时会被回收.
3.3 弱引用
弱引用也是用来描述非必须的对象的, 但它的强度更弱, 被弱引用关联的对象只能生存到下一次 GC 发生之前, 也就是说下一次 GC 就会被回收. JDK1.2 之后, 提供了 WeakReference 来实现弱引用.
3.4 虚引用
虚引用也成为幽灵引用或者幻影引用, 它是最弱的一种引用关系. 一个瑞祥是否有虚引用的存在, 完全不会对其生存时间造成影响, 也无法通过虚引用来取得一个对象的实例. 为一个对象设置虚引用关联的唯一目的就是在这个对象被 GC 时收到一个系统通知. JDK1.2 之后提供了 PhantomReference 来实现虚引用
4. 可能的内存泄露分析
了解了 ThreadLocal 的内部模型以及弱引用, 接下来可以分析一下是否有内存泄露的可能以及如何避免.
4.1 内存泄露分析
根据上一节的内存模型图我们可以知道, 由于 ThreadLocalMap 是以弱引用的方式引用着 ThreadLocal, 换句话说, 就是 ThreadLocal 是被 ThreadLocalMap 以弱引用的方式关联着, 因此如果 ThreadLocal 没有被 ThreadLocalMap 以外的对象引用, 则在下一次 GC 的时候, ThreadLocal 实例就会被回收, 那么此时 ThreadLocalMap 里的一组 KV 的 K 就是 null 了, 因此在没有额外操作的情况下, 此处的 V 便不会被外部访问到, 而且只要 Thread 实例一直存在, Thread 实例就强引用着 ThreadLocalMap, 因此 ThreadLocalMap 就不会被回收, 那么这里 K 为 null 的 V 就一直占用着内存.
综上, 发生内存泄露的条件是
ThreadLocal 实例没有被外部强引用, 比如我们假设在提交到线程池的 task 中实例化的 ThreadLocal 对象, 当 task 结束时, ThreadLocal 的强引用也就结束了
ThreadLocal 实例被回收, 但是在 ThreadLocalMap 中的 V 没有被任何清理机制有效清理
当前 Thread 实例一直存在, 则会一直强引用着 ThreadLocalMap, 也就是说 ThreadLocalMap 也不会被 GC
也就是说, 如果 Thread 实例还在, 但是 ThreadLocal 实例却不在了, 则 ThreadLocal 实例作为 key 所关联的 value 无法被外部访问, 却还被强引用着, 因此出现了内存泄露.
也就是说, 我们回答了文章开头的第一个问题, ThreadLocal 如果使用的不当, 是有可能引起内存泄露的, 虽然触发的场景不算很容易.
这里要额外说明一下, 这里说的内存泄露, 是因为对其内存模型和设计不了解, 且编码时不注意导致的内存管理失联, 而不是有意为之的一直强引用或者频繁申请大内存. 比如如果编码时不停的人为塞一些很大的对象, 而且一直持有引用最终导致 OOM, 不能算作 ThreadLocal 导致的 "内存泄露", 只是代码写的不当而已!
4.2 TheadLocal 本身的优化
进一步分析 ThreadLocalMap 的代码, 可以发现 ThreadLocalMap 内部也是做了一定的优化的
- /**
- * Set the value associated with key.
- *
- * @param key the thread local object
- * @param value the value to be set
- */
- private void set(ThreadLocal key, Object value) {
- // We don't use a fast path as with get() because it is at
- // least as common to use set() to create new entries as
- // it is to replace existing ones, in which case, a fast
- // path would fail more often than not.
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal k = e.get();
- if (k == key) {
- e.value = value;
- return;
- }
- if (k == null) {
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- tab[i] = new Entry(key, value);
- int sz = ++size;
- if (!cleanSomeSlots(i, sz) && sz>= threshold)
- rehash();
- }
可以看到, 在 set 值的时候, 有一定的几率会执行 replaceStaleEntry(key, value, i)方法, 其作用就是将当前的值替换掉以前的 key 为 null 的值, 重复利用了空间.
5. ThreadLocal 使用建议
通过前面几节的分析, 我们基本弄清楚了 ThreadLocal 相关设计和内存模型, 对于是否会发生内存泄露做了分析, 下面总结下几点建议:
当需要存储线程私有变量的时候, 可以考虑使用 ThreadLocal 来实现
当需要实现线程安全的变量时, 可以考虑使用 ThreadLocal 来实现
当需要减少线程资源竞争的时候, 可以考虑使用 ThreadLocal 来实现
注意 Thread 实例和 ThreadLocal 实例的生存周期, 因为他们直接关联着存储数据的生命周期
如果频繁的在线程中 new ThreadLocal 对象, 在使用结束时, 最好调用 ThreadLocal.remove 来释放其 value 的引用, 避免在 ThreadLocal 被回收时 value 无法被访问却又占用着内存
其实对于 ThreadLocalMap 还有很多设计, 关于其详细内容, 可以参考文后参考文章的最后一篇
参考文章
ThreadLocal 可能引起的内存泄露
反驳: Threadlocal 存在内存泄露 https://my.oschina.net/xpbug/blog/113444
理解 Java 中的 ThreadLocal
译文: 理解 Java 中的弱引用
深入理解 ThreadLocal (这些细节不应忽略)
来源: http://www.jianshu.com/p/1a5d288bdaee