ThreadLocal原理,内存泄露到底是怎么回事,干货满满

ThreadLocal原理(基础款)

ThreadLocal它提供了线程局部变量的功能。它的原理可以简单概括为以下几点:

  1. 每个Thread对象都维护了一个以ThreadLocal为键、任意类型对象为值的Map。这个Map存储了每个线程对应的局部变量值,这个Map叫做ThreadLocalMap。
  2. 当通过ThreadLocal对象调用set()方法设置变量值时,实际上是获取当前线程的ThreadLocalMap将当前ThreadLocal对象作为键,变量值作为值,存储到Map中。
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

3. 当通过ThreadLocal对象调用get()方法获取变量值时,实际上是获取当前线程的ThreadLocalMap,再以以当前的ThreadLocal对象为键获取对应的值。

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();
}

通过这种方式,ThreadLocal实现了线程间的数据隔离,每个线程都可以独立地访问和修改自己的局部变量值,而不会影响其他线程的变量。这在多线程编程中非常有用,可以避免线程间的数据竞争和冲突,提高程序的并发性能和可靠性。

TheadLocal跨线程的传递

ThreadLocal 的值是在每个线程中独立存储的,其他线程无法直接获取到该值。但是,你可以通过一些手段将 ThreadLocal 的值从一个线程传递到另一个线程,例如:

  1. 手动传递:可以在一个线程中获取 ThreadLocal 的值,然后将该值作为参数传递给另一个线程。这需要你自己管理传递过程和确保线程安全。
  2. 当你需要在父线程中设置一个 ThreadLocal 值,并使其在子线程中可用时,可以使用 InheritableThreadLocal 类型的 ThreadLocal 对象。InheritableThreadLocal 提供了一种方便的方式,在父线程和子线程之间传递 ThreadLocal 值,原理在父线程创建子线程时,线程初始化方法会将父线程的InheritableThreadLocal传递给子线程

image.png 需要注意的是,使用 ThreadLocal 进行跨线程传递值时,要确保线程安全性和正确性。在传递值的过程中,需要注意值的复制或引用传递、值的可变性、值的生命周期等问题,以避免意外的副作用和错误。

总之,虽然 ThreadLocal 的值不会自动在线程间传递,但你可以通过手动传递或使用特定的 ThreadLocal 子类来实现跨线程传递值的需求。在实际应用中,请注意线程安全性和正确性的考虑。

内存泄露原因

先从WeakReference说起

众所周知,java四种引用类型,强,软,弱,虚,先搞明白WeakReference什么时候会被gc清理,网上大多数的说法都是当GC时弱引用对象会被清理,但是需要前提条件,这点很重要!!

image.png jdk文档上写的大意就是噢,如果一个对象没有强、软引用(也要根可达),只通过弱引用(根)可达,那就可以被回收。
先搞几个测试类

image.png

image.png

weakAndSick为类变量,强引用在所以没被回收掉 image.png

强引用不在所以被回收掉 image.png

局部变量表中weakAndSickInner仍存在,存在根可达的强引用,所以回收不掉

image.png

method方法结束,局部变量引用断开,可以被回收

image.png

看看ThreadLocalMap代码结构

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

就是说entry继承WeakReference,也就是key是弱引用类型的,key对应的是ThreadLocal对象。 所以作者在这里使用弱引用类型,就是希望在ThreadLocal对象不再被使用时,ThreadLocalMap中的key中引用的ThreadLocal也就没有意义了,不要影响对ThreadLocal对象的垃圾回收!

内存泄露是针对ThreadLocalMap的,当然如果thread是临时线程用完就丢弃了,那自然可以回收ThreadLocalMap,但是如果是线程池中的常驻内存的线程呢?作为WeakReference的ThreadLocal对象可以被回收么? 实际上我们一般都将ThreadLocal对象作为一个类变量长期使用,所以依然存在强引用,在gc时仍不能回收。如果将这个强引用断开那么ThreadLocalMap的Key就可以被回收了,但是Value依然被强引用着,不能被回收。
所以在一些特殊场景下,也会有不一样的泄露的问题。 比如一个war包被部署到Tomcat中,Tomcat会加载包中的类,然后创建Servlet\Filrer这些类实例. Tomcat维护了一个线程池,每当有请求到来,这些线程对象就会去执行用于处理请求的task,也就是Filter\Servlt这些东西. 当一个war包被卸载时,Tomcat也应该释放所加载的Class和创建的Servlet/Filter实例,使之能被GC回收。
但是

一个类被GC回收需要保证以下条件都不成立:

  1. objects of that class are still reachable.
  2. the Class object representing the class is still reachable
  3. the ClassLoader that loaded the class is still reachable
  4. other classes loaded by the ClassLoader are still reachable

而ThreadLocal还保留着对象实例,导致类对象不能卸载,也会引起内存泄露。

避免 ThreadLocal 内存泄漏

  1. 了解使用场景:使用 ThreadLocal 时要了解其适用范围和生命周期,并确保在合适的时机清理和释放相关的值,及时调用 remove() 方法。
  2. 避免过度使用 ThreadLocal:仔细评估是否真正需要使用 ThreadLocal,尽量避免过度依赖它,以减少潜在的内存泄漏风险。

总之,虽然 ThreadLocal 在多线程编程中非常有用,但需要谨慎使用和管理。正确地使用和清理 ThreadLocal 的值可以避免内存泄漏问题,确保程序的性能和稳定性。

猜你喜欢

转载自juejin.im/post/7235117005737427001