ThreadLocal原理以及内存泄漏问题
什么是ThreadLocal?有哪些应用场景?
ThreadLocal
类可以让每个线程绑定自己的值,也就是拥有自己的专属本地变量。
ThreadLocal
为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,并且不会和其他线程的变量冲突,实现了线程间的数据隔离,避免了线程安全问题。
ThreadLocal
的应用场景主要有以下几个方面:
- 保存线程上下文信息,避免参数的显示传递,在需要的地方可以直接获取
- 线程间数据隔离
- 进行事务操作时存储线程事务信息,因为事务和线程绑定在一起(Spring在事务开始时会给当前线程绑定一个Jdbc Connection对象,放在ThreadLocal中存储,这样在整个事务执行过程中都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性)
- 数据库连接(经典的使用场景是为每个线程分配一个JDBC Connection连接对象,这样可以保证每个线程的都在各自的Connection上进行数据库的操作,不会出现A线程关了B线程正在使用的Connection)
- session会话等线程级别的操作(Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁)
ThreadLocal 原理
从Thread
类的源代码可以看出Thread
类中有一个 threadLocals
和一个 inheritableThreadLocals
变量,它们都是 ThreadLocalMap
类型的变量,实际上当前线程调用 ThreadLocal
类的 set
或get
方法时,我们调用的是当前线程的ThreadLocalMap
类对应的 get()
、set()
方法。
1 | public class Thread implements Runnable { |
ThreadLocal 是线程本地存储,每个线程中都具备一个ThreadLocalMap
,ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。ThreadLocalMap
是ThreadLocal
的内部类,可以理解为一个Map容器,其维护了一个 Entry 数组,由一个个key-value对象Entry
构成。
由于每一条线程都含有线程私有的ThreadLocalMap容器,这些容器间相互独立不影响,因此不会存在线程安全的问题,从而无需使用同步机制来保证多条线程访问容器的互斥性。
- 使用set方法时:
ThrealLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象,再以当前的ThreadLocal对象为key,将值存入ThreadLocalMap对象中。
- get方法执行过程类似,首先ThreadLocal获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前的ThreadLocal对象为key,获取对应的value。
1 | public void set(T value) { |
ThreadLocal 内存泄漏问题
内存泄露 :指的是为程序在申请内存后,无法释放已申请的内存空间。一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存迟早会被占光。简单来说,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
与OOM的区别:内存泄漏是内存占用无法释放,而OOM是内存不够,内存泄漏会导致OOM。
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry,而value还存在着强引用。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后最好手动调用remove()
方法。
看到Entry继承自 WeakReferencr<ThreadLocal<?>>
,就是一个 key-value 形式的对象。它的 key 就是 ThreadLocal 对象,并且是一个弱引用,如果没有指向 key 的强引用后,该 key 就会被垃圾回收器回收;Entry 的 value 就是存储 Object 对象,是强引用。
1 | static class ThreadLocalMap { |
强引用:
指在代码之中普遍存在的引用赋值,即使用
new
对象创建强引用。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会对被引用的对象进行回收。
1 Object obj = new Object();如果想要取消强引用和某个对象之间的关联,可以显示将引用赋值为null,或者超过了引用的作用域时,如方法结束,就可以当作垃圾回收,这样JVM就可以在合适的时间对其回收。
弱引用:
也是用来描述那些非必须对象,但被弱引用关联的对象不管内存是否足够都一定会被回收,也就是说它只能存活到下一次垃圾回收发生为止,比起软引用,只具有弱引用的对象拥有更短暂的生命周期。可以使用
WeakReference
类来创建弱引用。
为什么要将key设计成ThreadLocal的弱引用?
如果key
是强引用,同样会发生内存泄漏的。引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
如果是弱引用的话,引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收,此时的key为null,但在下一次调用ThreadLocalMap的set()、get()、remove()方法时,会清除 key 为 null 的 value 值,避免内存泄漏。
因此,ThreadLocal内存泄漏的根本原因是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
所以两种方案比较下来,还是ThreadLoacl
的key
为弱引用好一些。
ThreadLocal的正确使用方法:
- 当ThreadLocal作为局部变量时,每次使用完(方法结束)都调用其 remove() 方法清除数据(生命周期不需要和项目的生存周期一样长的)。
- 将ThreadLocal变量定义成为private static,这样就一直存在ThreadLocal的强引用,ThreadLocal就不会轻易被回收,可以保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉无用的value。