不人云亦云&独立思考ThreadLocal



简介

介绍ThreadLocal原理并思考内存泄漏问题。

ThreadLocal作用

在同一个线程共享一个全局资源,如常见的数据库连接池。 如在Spring的Service调用Dao层,是事务管理器通过ThreadLocal从数据库连接池获取数据库连接,作为线程的全局变量传递给Dao层。

ThreadLocal原理

ThreadLocal.java里面包含静态内部类

1
2
3
4
5
6
7
8
9
10
11
public class ThreadLocal<T> {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

ThreadLocalMap是ThreadLocal的内部静态类,但却被Thread类包含,不是一个好设计,但也没其他更好的办法了。 这里key是弱引用,作用就是当且仅当ThreadLocalMap实例引用key实例时,若触发GC,key实例会被回收。ThreadLocalMap的key有可能为null,ThreadLocal的get、set、remove会触发expungeStaleEntry()清理部分key为null的entry。

ThreadLocalMap这个变量被Thread内部管理。ThreadLoacl获取ThreadLocalMap都是通过getMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

public class ThreadLocal<T> {
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}

Thread类里面存储了map,以ThreadLocal作为key,资源为value。当ThreadLocal调用get方法时,就可以获取线程相关的资源,故可保存同一个线程的全局变量。

这里不讲解源码,源码具体实现请自行查看。 ThreadLoca为什么不用HashMap而要独自创建专属的ThreadLocalMap? 答:最主要原因就是ThreadLocalMap的key是需要弱引用(原因见下节),HashMap显然不合适。

ThreadLocalMap是一个单纯的数组table,存储了Entry对象,解决hash冲突是用开放定址法(线性补偿探测法)而不是Hashmap的拉链法。 用开放定址法而不用拉链法的原因,这里我猜测是,key有可能变为null,开放定址法可以利用已经为null的空间提高table的利用率, 而拉链法解决冲突需要额外的空间存储链表,rehash过程比开放定址法复杂,后续处理key为null的过程也没有开放定址法简单。

ThreadLocal是否会内存溢出

ThreadLocal的一般应用如下图 ThreadLocal 可以看出ThreadLocal1是Thread1和Thread2的共同资源,这里一般是类里面的静态资源。 ThreadLocalMap的生命周期和线程的生命周期是一致的。

上图分3种情况

  1. thread1和2是一次性的线程,用完即销毁;
  2. thread1和2是守护线程或者在线程池运行,局部资源ThreadLocal2,3;
  3. thread1和2是守护线程或者在线程池运行,静态资源ThreadLocal1。

3种情况分析

  1. 第1种情况thread销毁,Thread1,2、Entry1,2,3,4、Object1,2,3,4和ThreadLocal2,3均被GC回收(ThreadLocal1除外)。
  2. 第2种情况若Application释放了局部资源ThreadLocal2和3,ThreadLocal2和3自然会被下一次GC回收。后续在线程中对ThreadLocal操作的get、set和remove会清理掉Object1和3的对应的Entry。若随后Application释放了Object1和3则Object1和3可以被GC回收;

  3. 第3种情况只有Application主动调用静态资源ThreadLocal1的remove才会把threadLocalMap引用的Entry删除,若随后Application释放了Object2和4则Object2和4可以被GC回收。 第3种情况等价于Entry的key是强引用,因key是静态资源无法被gc回收。可得ThreadLocalMap的key弱引用是ThreadLocal的一种优化,释放掉局部资源TheadLocal,从而可以释放Object。

大部分框架都是运用于第3种线程池的情况,框架里面都会主动调用ThreadLocal的remove,避免Object资源常驻对应线程造成上下文Object资源混乱。

总结

  1. Thread类和ThreadLocal耦合,且ThreadLocal的生命周期可能是一瞬间的。ThreadLocalMap的key弱引用是为了“自动”清理Thread类里面引用的ThreadLocal实例。
  2. ThreadLocalMap的key弱引用设计不会诱发内存泄漏。如果在常驻的线程中不再操作!任何!ThreadLocal,我认为“泄露”的对象如Object1和3以及他们的引用对象已经是此线程的“资源”,GC不应该回收他们。ThreadLocal的设计已经非常好,搞不懂为什么网上各种黑ThreadLocal的内存泄漏= =!!