多线程 - ThreadLocal 是什么?有哪些使用场景?
ThreadLocal常用API
- void set (Object value)设置当前线程的线程局部变量的值。
- public Object get() 该方法返回当前线程所对应的线程局部变量。
- public void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它 可以加快内存回收的速度。无论是 get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法。
- protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为 了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get() 或 set(Object)时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一 个 null。
- 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 的创建,初 始化等工作。
详细解释
![](https://img2020.cnblogs.com/i-beta/1552449/202003/1552449-20200314162908284-454323313.png)
上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap, ThreadLocalMap 是 ThreadLocal 的静态内部类,然后 Thread 类中有一个这样类型 成员,所以 getMap 是直接返回 Thread 的成员。
看下 ThreadLocal 的内部类 ThreadLocalMap 源码:
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 方法。
从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得 思考:为什么使用弱引用而不是强引用?
为什么使用弱引用而不是强引用?
从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得 思考:为什么使用弱引用而不是强引用。
下面我们分两种情况讨论:
- key 使用强引用:对 ThreadLocal 对象实例的引用被置为 null 了,但是 ThreadLocalMap 还持有这个 ThreadLocal 对象实例的强引用,如果没有手动删除, ThreadLocal 的对象实例不会被回收,导致 Entry 内存泄漏。
- 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