深度思考ThreadLocal

1 推荐

threadLocal变量详解

ThreadLocal为什么要使用弱引用和内存泄露问题

2 ThreadLocal的工作原理是:

每个Thread维护一个ThreadLocalMap,这个ThreadLocalMap的键是ThreadLocal对象,值是实际需要存储的Object。也就是说,虽然所有的线程都能访问到同一个ThreadLocal对象,但是每个线程在ThreadLocalMap中存取的实际Object都是独立的。

3 这种存储结构的好处:

1、线程死去的时候,线程共享变量ThreadLocalMap则销毁, 也就是说ThreadLocalMap存储的各个ThreadLocal声明的数据副本的生命周期更短,能够节省内存,也适合大量端生命周期的线程任务的执行。

2、ThreadLocalMap键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),存储性能提高很多,为什么呢?如果在ThreadLocal中用Map键值对存储线程共享变量,并发量很大的情况,一个threadLocal的Map的size会很大,而且会扩容但是不会缩小,即使线程使用完后remove对应的对象,Map占用的空间仍然不会释放。

4 关于ThreadLocalMap弱引用问题:

当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。

虽然ThreadLocal的get,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用也就是说ThreadLocal设置为弱引用问题本身就是为了解决自身的内存泄露问题,但是解决的还不够彻底

4.1 为什么会当线程没有结束,但是ThreadLocal已经被回收?

在Java中,当一个对象只被弱引用(WeakReference)引用时,那么在下一次垃圾回收时,这个对象就会被回收。ThreadLocal在ThreadLocalMap中使用的是弱引用,因此可能出现ThreadLocal对象被回收,但线程还在运行的情况。

4.2 强软弱虚四种引用都介绍一下

Java中的引用类型分为四种:

- **强引用(Strong Reference)**:这是最常见的引用类型。如果一个对象有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会回收这种对象。

- **软引用(Soft Reference)**:如果一个对象只具有软引用,那么在系统将要发生内存溢出异常之前,这些软引用的对象列将会被回收。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用主要用于实现内存敏感的缓存。

- **弱引用(Weak Reference)**:如果一个对象只具有弱引用,那么在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。弱引用和软引用的区别在于,软引用在内存不足时才会被回收,而弱引用不考虑内存是否足够。

- **虚引用(Phantom Reference)**:一个持有虚引用的对象,和没有任何引用一样,在任何时候都可能被垃圾回收,虚引用主要用来跟踪对象被垃圾回收的活动,需要用java.lang.ref.PhantomReference类来使用。

4.3 为什么threadLocal要被设置为弱引用?

ThreadLocal被设置为弱引用的主要是为了解决自身在不被需要时无法被gc,导致内存泄漏的问题。如果ThreadLocal为强引用,那么只要ThreadLocal对象还在,就会持续对ThreadLocalMap中的value持有强引用,导致即使ThreadLocal没有被外部引用,也无法被垃圾回收。而设置为弱引用后,ThreadLocal没有被外部强引用时,就会被垃圾回收器回收,进而可能间接(这里取决于后续还有没有get,put操作)释放ThreadLocalMap中的value。

4.4 可能大家会有疑惑,4开头的部分说了threadLocal设置为弱引用还是会导致内存泄漏,那为什么还要设置threadLocal为弱引用呢?

答:ThreadLocal设置为弱引用问题是为了解决自身的内存泄露问题,但是有副作用,即还是有概率导致threadLocal对应的value也发生了内存泄露。所以需要我们进一步完善它。这里的概率是指ThreadLocal回收之后再也没有get,put调用对value进行懒回收。

5 解决方案

所以为了防止此类情况的出现,我们有两种手段。

1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量(指的是value)。

2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了,但是仍然会有开头介绍的生命周期拉长导致即使是线程销毁,但是ThreadLocal类对象仍然存活的内存泄露问题,这跟对ThreadLocal强引用引起内存泄漏的概率还要大,所以不推荐使用。

5.1 这里的两种方法,分别适用于什么场景

  1. 使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量:

    这种方法主要适用于临时使用ThreadLocal的场景,如在一段需要用到线程局部变量的代码块中使用ThreadLocal,完成操作后立即清除,避免ThreadLocalMap中出现无用的键值对,造成内存泄漏。这种方法适用于对ThreadLocal使用控制较为精细的场景。

  2. JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了:

    这种方法主要适用于ThreadLocal在类的整个生命周期中都可能被使用到的场景。通过将ThreadLocal定义为private static,可以确保ThreadLocal对象的生命周期与其所在的类相同,避免因为ThreadLocal对象被提前回收,导致线程之间数据丢失的问题。这种方法适用于ThreadLocal的生命周期较长,或者其生命周期需要与类的生命周期一致的场景

总的来说,选择哪种方法主要取决于你的实际需求,以及对ThreadLocal的使用控制能力。在实际使用中,也可以将两种方法结合起来,既定义为private static,又在使用完成后调用remove方法,这样可以最大程度地避免内存泄漏的发生。

5.2 为什么“JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了”?

JDK建议ThreadLocal定义为private static的原因是,这样可以延长ThreadLocal实例的生命周期。如果ThreadLocal是静态的,那么它的生命周期就和类的生命周期一样长,即只有当类被卸载时,ThreadLocal才会被回收。这样,就避免了ThreadLocal实例被过早回收的问题,也就是所说的"弱引用问题"。此外,由于ThreadLocal不持有任何线程的引用,所以也不会因为ThreadLocal存在而阻止线程被回收,这避免了线程泄露的问题。

你可能感兴趣的:(jvm,java,开发语言)