深度思考ThreadLocal

1 推荐

threadLocal变量详解

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

2 ThreadLocal的工作原理是:

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

3 这种存储结构的好处:

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

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

4 关于ThreadLocalMap<ThreadLocal, Object>弱引用问题:

当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(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存在而阻止线程被回收,这避免了线程泄露的问题。

6 除了内存泄漏,threadLocal还有没有问题(重点:淘天客满二面)

细数 ThreadLocal 三大坑,内存泄露仅是小儿科

7 内存泄露的一些疑问

7.1 既然threadLocalMap中的key在下一次gc时会被清理,那是不是存在线程突然获取不到这个threadLocal变量的情况,那线程怎么办

首先,我们需要理解一下ThreadLocalThreadLocalMap的工作方式。

ThreadLocal中,键(key)是一个对ThreadLocal对象的弱引用(WeakReference)。这意味着,在没有其他强引用指向ThreadLocal对象时,这个对象在下一次GC(Garbage Collection)时是可以被回收的。但这并不意味着与这个ThreadLocal对象关联的值会立即被移除。

ThreadLocal对象被回收后,对应的键会变为null,但值还会留在ThreadLocalMap中,直到下一次对ThreadLocalMap的操作(如set, get, remove等)。在这些操作过程中,ThreadLocalMap会尝试清理这些无效的条目(即键为null的条目)。

现在,回到你的问题:

既然threadLocalMap中的key在下一次gc时会被清理,那是不是存在线程突然获取不到这个threadLocal变量的情况?

只有在你失去了对ThreadLocal对象的所有强引用,并且GC运行后,这种情况才会发生。但在这种情况下,获取不到ThreadLocal变量其实是预期的行为,因为你已经没有对该ThreadLocal对象的引用了。

如果你希望ThreadLocal的值在整个线程的生命周期中都存在,你需要确保:

  1. ThreadLocal对象保持一个强引用,以防止它被GC回收。
  2. 在不再需要ThreadLocal的值时,明确地调用ThreadLocalremove()方法,以释放与该线程关联的值。

如果你意外地失去了对ThreadLocal的引用并且需要它的值,那你需要重新计算或重新设置它的值。但通常,正确的做法是确保在使用ThreadLocal时始终保持对其的引用。

7.2 只有在你失去了对ThreadLocal对象的所有强引用,并且GC运行后,这种情况才会发生。但在这种情况下,获取不到ThreadLocal变量其实是预期的行为,因为你已经没有对该ThreadLocal对象的引用了。怎么知道失去了对ThreadLocal对象的所有强引用,是线程执行完了就失去了吗?

当我们谈论失去对ThreadLocal对象的所有强引用时,我们是指从应用程序的上下文(非ThreadLocalMap部分)来看,不再有任何引用指向该ThreadLocal对象。这与线程是否执行完成无关。

举个简单的例子:

public class Sample {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public void useThreadLocal() {
        threadLocal.set(42);
        Integer value = threadLocal.get();
        System.out.println(value);
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.useThreadLocal();
    }
}

在这个例子中,threadLocal是一个静态变量,因此在Sample类的生命周期内它都持有对ThreadLocal对象的强引用。这意味着,只要Sample类没有被卸载(通常在长时间运行的应用程序或容器中可能发生),ThreadLocal对象都不会被GC回收。

但是,如果我们这样做:

public class Sample {

    public void useThreadLocal() {
        ThreadLocal<Integer> localThreadLocal = new ThreadLocal<>();
        localThreadLocal.set(42);
        Integer value = localThreadLocal.get();
        System.out.println(value);
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.useThreadLocal();
        // 一旦退出useThreadLocal方法,localThreadLocal已经超出了其作用范围,它就不再有强引用指向它。
    }
}

在这个例子中,一旦useThreadLocal方法执行完毕,localThreadLocal变量超出了其作用范围,因此没有强引用指向该ThreadLocal对象了。这意味着在下一次GC运行时,这个ThreadLocal对象可能会被回收。

所以,关键是要理解从哪里和如何引用ThreadLocal对象。只有当没有强引用指向它时,它才会在下次GC时被回收。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值