—— ThreadLocal
基本概念
- ThreadLocal 提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问 ThreadLocal 实例的时候(通过其get 或者 set 方法)都有自己的、独立初始化的变副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如:用户Id或者 事务ID)与线程关联起来
- 实现每一个线程都有自己专属的本地变量副本,主要解决了让每个线程绑定自己的值,通过get 和 set 方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题
- 既然其他 Thread 不访问,那就不存在多线程之间的共享问题
- 统一设置初始值但是每个线程对这个值的修改都是各自线程互相独立的
Thread、ThreadLocal、ThreadLocalMap 三者关系
- ThreadLocalMap 实际上就是一个以 ThreadLocal 实例为 key,任意对象为 value 的 Entry 对象
- 当我们为 ThreadLocal 变量赋值,实际上就是以当前 ThreadLocal 实例为 key,值为 value 的 Entry 往这个 ThreadLocalMap 中存放
- Jvm 内部维护了一个线程版的 Map<ThreadLocal,Value>,每个线程要用到这个 T 的时候,用当前的线程去 Map 里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量
引发内存泄露问题
- 内存泄露:不再会被使用的对象或者变量占用的内存不能被回收
- ThreadLocalMap 从字面可以看出这是一个保存 ThreadLocal 对象的 map(以 ThreadLocal 为 key),不过是经过了两层包装的 ThreadLocal 对象
- 第一层包装是使用
WeakReference<ThreadLocal<?>>
将 ThreadLocal 对象变成一个弱引用对象 - 第二层包装是定义了一个专门的类 Entry 来扩展
WeakReference<ThreadLocal<?>>
- 第一层包装是使用
- 强引用:当内存不存,JVM 开始垃圾回收,对于强引用的对象,就算出现了 OOM 也不会对该对象进行回收;最常见的普通对象引用,只要还有强引用指向一个对象,就表明对象活着,除非为null。因此强引用是造成内存泄露的主要原因之一
- 软引用:相对强引用弱化,用
java.lang.ref.SoftReference
实现,通常用在对内存敏感的程序中,比如高速缓存就需要软引用;JVM 内存充足时它不会被回收,不足时会被回收 - 弱引用:用
java.lang.ref.WeakReference
实现,比软引用的生存期更短,只要垃圾回收机制一运行, 不管JVM 的内存空间是否足够,都会被回收- 软引用 和 弱引用的适用场景:读取大量的本地图片
- 如果每次读取图片从硬盘读取影响性能;一次性全部加载到内存中可能造成内存溢出
- 软引用可解决该问题:用
HashMap<String,SoftReference<BitMap>>
来保存图片的路径和响应的图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效避免 OOM 的问题
- 软引用 和 弱引用的适用场景:读取大量的本地图片
- 虚引用:用
java.lang.ref.PhantomReference
实现- 必须和引用队列(ReferenceQueue)联合使用,形同虚设;一个对象仅持有虚引用,那么它和没有任何引用一样,在任何时候都可能被垃圾回收器回收
- PhantomReference 的get方法总是返回 null,因此无法访问对象的引用对象
- 设置虚引用的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理
- 为什么使用弱引用?不用有什么后果
- 当线程方法执行完毕后,栈帧销毁强引用 tl 就没有了,但此时线程的ThreadLocalMap 里某个 entry 的key 引用还指向这个对象
- 若 key 为强引用,导致key 指向的 ThreadLocal 对象 和 v 不能被 GC,造成内存泄露
- 若 key 为弱引用就大概率减少内存泄露的问题,使用弱引用,就可以使 ThreadLocal 对象在方法执行完毕后顺利回收且Entry的key引用指向为null
- 当 key 引用指向为null时,同样会引发内存泄露!!
- 当ThreadLocalMap 中出现 key 为null 的 Entry ,就没有办法访问这些 key 为 null 的 Entry 的value,如果当前线程迟迟不结束的话,这些 key 为 null 的 Entry-value 就会一直存在一条强引用链(Thread Ref 》 Thread 》 ThreadLocalMap 》 Entry 》 value),永远无法回收,造成内存泄露
- 如果当前 Thread 结束,Entry 没有引用链可达,在垃圾回收的时候就会被系统进行回收
- 但在实际应用中,我们会用线程池维护我们的线程,比如在
Executors.newFixedThreadPool()
时创建线程时,为了复用线程是不会结束的,所以ThreadLocal内存泄露就值得注意 - 解决:要在不适用某个 ThreadLocal 对象后,手动调用 remove 方法来删除(remove方法会寻找脏 Entry(key=null) ,然后进行删除)
最佳实践方式
- 使用
ThreadLocal.withInitial(() -> 初始化值)
初始化 - 建议把 ThreadLocal 修饰为 static (ThreadLocal 只需要初始化一次,不需要多次)
- 手动调用 remove
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal 并不解决线程间共享数据的问题
- ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题