多线程 ThreadLocal 是什么?有哪些使用场景?

ThreadLocal常用API

  1. void set (Object value)设置当前线程的线程局部变量的值。
  2. public Object get() 该方法返回当前线程所对应的线程局部变量。
  3. public void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它 可以加快内存回收的速度。无论是 get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法
  4. protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为 了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get() 或 set(Object)时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一 个 null。
  5. public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();RESOURCE 代表一个能够存放 String 类型的 ThreadLocal 对象。 此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

 

为什么说“get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的”?

【这里写的不确定,请大家斧正】以get()为例,只有当被置为null的ThreadLocal自己调用get时,才会走入getEntryAfterMiss()的逻辑。其他ThreadLocal变量调用get不会清除。

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();
    }

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                //清除key为null的Entry
                return getEntryAfterMiss(key, i, e);
        }

 

ThreadLocal适用场景

ThreadLocal在spring的事务管理,包括Hibernate的session管理等都有出现,在web开发中,有时会用来管理用户会话 HttpSession,web交互中这种典型的“一请求一线程”的场景似乎比较适合使用ThreadLocal,但是需要特别注意的是,由于此时session与线程关联,而tomcat这些web服务器多会采用线程池机制,也就是说线程是可复用的,所以在每一次进入的时候都需要重新进行set,或者在结束时及时remove。

ThreadLocal底层实现原理

框架图

  • 重点:记住这张图!!!!
  • Entry里的key保存的是Threadlocal的弱引用。
  • Entry里的value保存的是强引用,保存的也是引用,不是实例。

 

用get()方法描述底层原理

总结

get 方法,其实就是拿到每个线程独有的 ThreadLocalMap,然后再用 ThreadLocal 的当前实例,拿到 Map 中的相应的 Entry,然后就可 以拿到相应的值返回出去。当然,如果 Map 为空,还会先进行 map 的创建,初 始化等工作。 

详细解释

上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap, ThreadLocalMap 是 ThreadLocal 的静态内部类,然后 Thread 类中有一个这样类型 成员,所以 getMap 是直接返回 Thread 的成员。

看下 ThreadLocal 的内部类 ThreadLocalMap 源码: 

可以看到有个 Entry 内部静态类,它继承了 WeakReference,总之它记录了 两个信息,一个是 ThreadLocal<?>类型,一个是 Object 类型的值。getEntry 方法 则是获取某个 ThreadLocal 对应的值,set 方法就是更新或赋值相应的 ThreadLocal 对应的值。

ThreadLocal引发的内存泄漏

总结

ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引 用。 

  • JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
  • JVM 利用调用 remove、get、set 方法的时候,回收弱引用。 
  • 当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。
  • 使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的 重复运行的,从而也就造成了 value 可能造成累积的情况。

详细分析

根据我们前面对 ThreadLocal 的分析,我们可以知道每个 Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal 实例本身(应该是 ThreadLocal 实例的虚引用),value 是真正需 要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察 ThreadLocalMap,这个 map 是使用ThreadLocal的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。 因此使用了 ThreadLocal 后,引用链如图所示:

图中的虚线表示弱引用。

这样,当把 threadlocal 变量(Thread Local Ref)置为 null 以后,没有任何强引用指向 threadlocal 实例(红色方块 Thread Local),所以 threadlocal实例将会被 gc 回收。这样一来,ThreadLocalMap 中就会出现

key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前 线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强 引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永 远不会被访问到了,所以存在着内存泄露。

只有当前 thread 结束以后,current thread 就不会存在栈中,强引用断开, Current Thread、Map value 将全部被 GC 回收。最好的做法是不在需要使用 ThreadLocal 变量后,都调用它的 remove()方法,清除数据。

所以回到我们前面的实验场景,场景 3 中,虽然线程池里面的任务执行完毕 了,但是线程池里面的 5 个线程会一直存在直到 JVM 退出,我们 set 了线程的 localVariable 变量后没有调用 localVariable.remove()方法,导致线程池里面的 5 个 线程的 threadLocals 变量里面的 new LocalVariable()实例没有被释放。

其实考察 ThreadLocal 的实现,我们可以看见,无论是 get()、set()在某些时 候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法。

从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得 思考:为什么使用弱引用而不是强引用? 

为什么使用弱引用而不是强引用? 

从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得 思考:为什么使用弱引用而不是强引用。

下面我们分两种情况讨论:

  1. key 使用强引用:对 ThreadLocal 对象实例的引用被置为 null 了,但是 ThreadLocalMap 还持有这个 ThreadLocal 对象实例的强引用,如果没有手动删除, ThreadLocal 的对象实例不会被回收,导致 Entry 内存泄漏。
  2. key 使用弱引用:对 ThreadLocal 对象实例的引用被被置为 null 了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的 对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都 有机会被回收。

比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可 以多一层保障。

因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引 用。 

错误使用 ThreadLocal 导致线程不安全

错误原因:Entry里的value保存的是强引用,保存的也是引用,不是实例。因此不同的线程在ThreadLocalMap保存的还是引用。由于这里保存的是一个static的变量,因此不同的线程保存的value引用都指向同一个对象。

修改方法:把number的static去掉。

详细解释:

为什么每个线程都输出 5? 难道他们没有独自保存自己的 Number 副本吗? 为什么其他线程还是能够修改这个值?

仔细考察 ThreadLocal 和 Thead 的代码, 我们发现 ThreadLocalMap 中保存的其实是对象的一个引用,这样的话,当有其 他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有 的对象引用所指向的同一个对象实例。这也就是为什么上面的程序为什么会输出 一样的结果:5 个线程中保存的是同一 Number 对象的引用,在线程睡眠的时候, 其他线程将 num 变量进行了修改,而修改的对象 Number 的实例是同一份,因 此它们最终输出的结果是相同的。

而上面的程序要正常的工作,应该的用法是让每个线程中的 ThreadLocal 都 应该持有一个新的 Number 对象。 

ThreadLocal在Spring事务管理中的应用 

Spring如何处理模板类的Bean(JDBC etc...),在多线程下的并发安全问题?

适用ThreadLocal。。。TODO: find answer from ppt、MP4

参考

谈谈Java中的ThreadLocal: https://www.cnblogs.com/chengxiao/p/6152824.html

ThreadLocal在Spring事务管理中的应用:https://www.cnblogs.com/fishisnow/p/6396989.html

原文地址:https://www.cnblogs.com/frankcui/p/10820066.html