ThreadLocal为什么存在内存泄漏,源码分析

本文围绕Java的ThreadLocal展开,介绍其用于隔离线程变量和传递数据的使用场景,分析弱引用与内存泄露问题,对ThreadLocalMap的源码进行解读,包括set、get、remove操作。还提及InheritableThreadLocal解决继承性问题,并总结了ThreadLocal的常见问题及解决办法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. ThreadLocal的使用场景

 
通常,我们使用ThreadLocal有两个目的:

  • ①:用来隔离不同线程的变量,避免线程间互相干扰。
    • 比如处理登陆信息,每次用户请求都带有用户信息。通常我们都是一个线程处理一个用户请求,所以我们可以把用户信息放到Threadlocal里,让每个线程处理自己的用户信息,线程之间互不干扰。
    • Spring用它来管理数据库连接,每个线程获取的都是自己的数据库连接对象。
  • ②:使用ThreadLocal来传递数据。
    • 比如,一个典型的用户请求需要经过拦截器–>controller–>service–>dao。如果想在这条请求链上传递数据,可以使用参数的方式一直传递下去,但这样做不太优雅。这时就可以使用ThreadLocal来存数据,由于一次请求只会对应一个线程,数据是与线程绑定的 。后续可以再通过ThreadLocal取出数据。

下面是jdk文档对ThreadLocal的描述。

ThreadLocal类提供了线程局部变量,这些变量不同于通常的变量,每个线程访问(通过get/set方法)的都是线程各自独有的变量副本。

ThreadLocal实例通常都是类中私有的静态的(private static)成员变量。这些类希望将状态与线程关联(例如,用户id或事物id)。

例如,下面的类将会为每个线程产生一个独有的标识符(id)。线程id在首次调用ThreadId.get()方法时分配,并且在随后的调用中保持不变。

 

2. 弱引用与内存泄露

 
Thread 和 ThreadLocal 中对象的关系图如下:

在这里插入图片描述

弱引用回收时机:当一个对象只被弱引用关联,而没有强引用指向它时,​​下一次垃圾回收(GC)发生时,这个对象会被回收​​。ThreadLocal中的Entry中的key(ThreadLocal对象)被弱引用关联。这意味着:如果外部没有任何强引用指向这个ThreadLocal对象(比如ThreadLocal变量被置为null,或者不再被任何其他对象引用),那么在下一次GC时,这个ThreadLocal对象会被回收。

ThreadLocal的具体回收场景如下:

  • 场景1:ThreadLocal实例被显式置null​​,之前创建的ThreadLocal对象没有强引用了,如果发生GC,Entry中的key(ThreadLocal对象)就会被回收

    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    threadLocal = null; // 此时,之前创建的ThreadLocal对象没有强引用了
    System.gc(); // 触发GC,该ThreadLocal对象被回收(key变成null)
    
  • 场景2:ThreadLocal实例在方法内创建,方法退出,局部变量threadLocal超出作用域,ThreadLocal对象失去强引用,后续GC时,该ThreadLocal对象被回收。

    void method() {
        ThreadLocal<String> threadLocal = new ThreadLocal<>(); // 局部变量
        threadLocal.set("value");
    } 
    // 方法结束,局部变量threadLocal超出作用域,ThreadLocal对象失去强引用
    // 后续GC时,该ThreadLocal对象被回收
    System.gc();
    

当调用ThreadLocal.set(value)时,引用关系如下图所示:

在这里插入图片描述

需要说明的是,这里的弱引用指的既不是Entry也不是value,而是key,也就是key对ThreadLocal实例存在弱引用。
       

2.1 为什么要将ThreadLocalMap的key设计为弱引用类型呢?

       假设key不是弱引用,而是强引用,由于强引用的存在,那么在Thread–>ThreadLocalMap–>Entry在这条强引用链下,只有等待线程Thread结束,这条强引用链消失,entry不可达被gc,最终Threadlocal对象也会不可达才会被gc。如果是这样的话,岂不是ThreadLocal对象的回收要看线程的执行时间了。

       如果是通过new Thread()的方式使用ThreadLocal就还好,这条引用链也会随着线程的销毁而被GC。但假如线程生命周期较长,比如项目中经常用的线程池中的线程,entry中的key虽然已经被之前的业务线程使用完毕,但是却无法回收,因为线程并不销毁,还要被其他任务复用,但之前任务在里边存储的东西其实已经没有用了,之前任务在里边存储的东西有以下两种情况:

  • 如果存储的是通过 new 创建的 非static 的ThreadLocal对象,假设key是强引用,那强引用链一直存在,无法被GC,导致key的内存泄漏
  • 如果存储的是全局声明的static final 的ThreadLocal对象,假设key是强引用,强引用链也一直存在,无法被GC,导致key的内存泄漏。
    • 因为这个对象本身带有static final,生命周期很长,基本不会被GC,这种情况下看似与key设计为弱引用类型观念相悖,但在很多场景下却是经常使用的。因为它可以避免重复创建ThreadLocal,节省内存。但也需要业务方配合清理,否则会出现value的内存泄漏、数据污染等问题!下文会有详细解释

所以Entry的key(ThreadLocal)需要被设置成弱引用,弱引用能保证没有被强引用的Threadlocal对象一定活不过下次gc,一定会被回收掉。所以说,将ThreadLocalMap的key设计为弱引用,能在一定程度上防止内存泄露,这里的泄露指的是ThreadLocal对象的泄露。

       当业务中 ThreadLocal的引用为null 时,ThreadLocal 实例​​就仅被弱引用关联​了,如果发生GC会立即回收 ThreadLocal 对象,此时的Entry.key 自动被设为 null,后续通过 ThreadLocalMap 操作(get/set/remove)会检测并清理 key==null 的 Entry,这样就防止了内存泄漏。

       

2.1 ThreadLocalMap的key设计为弱引用类型可一定程度上防止内存泄露,而不是说最终保证不会发生内存泄露?

       别忘了还有value对象。业务方必须自己管理value的生命周期!由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经被回收掉了,但还存在Thread-->ThreadLocalMap-->Entry-->value这样的强引用链,value在留存在线程的ThreadLocalMap的Entry中。即存在key为null而value却有值的无效Entry,注意这个Entry是强引用的!value在这个entry中, entry不消失,或者不手动清除value,value就一直不消失,这样导致value的内存泄漏。(由于value只能通过ThreadLocal的set/get/remove方法来访问,当ThreadLocal对象因为弱引用的原因被回收后,value自然也就无法再被访问到,成了无用资源了。)

       所以,ThreadLocal采取了一定的措施来尽量避免内存泄露的发生。每次调用ThreadLocal的get/set/remove方法时,都会触发执行expungeStaleEntry方法,对失效(key为null)的Entry的做清理工作:擦除Entry(置为null),同时检测整个Entry数组将key为null的Entry一并擦除,然后重新调整索引。但是,只有在调用这三个方法才会触发清理,而实际上很可能我们在使用完ThreadLocal之后就不再做任何操作了,这样就不会触发ThreadLocal的清理工作。所以,当我们使用完ThreadLocal后,尽量手动调用一下remove方法,尽早地将value清理掉。

       弱引用(key)会在​​没有其他强引用指向该ThreadLocal对象​​时,在​​下一次GC​​中被回收。然而,value需要依赖手动remove或自动清理机制来回收,否则可能泄漏。

remove()解决value内存泄露

public class TenantHolder {

    private static final ThreadLocal<Session> CONTEXT = new ThreadLocal<>();

    public static void setContext(Session session) {
        CONTEXT.set(session);
    }

    public static Session getContext() {
        return CONTEXT.get();
    }

	//使用完后,手动调用一下这个清除的方法
    public static void clear() {
        CONTEXT.remove();
    }
}

       

2.3 为什么建议使用 static final 声明ThreadLocal

    //正确声明
    private static final ThreadLocal<Session> CONTEXT = new ThreadLocal<>();
  • 避免重复创建ThreadLocal,节省内存
    • 使用static修饰的变量,属于线程共享的。在多线程环境下,如果每次请求创建新ThreadLocal,在高并发请求下,容易造成内存急剧标高。使用static创建的ThreadLocal可以拿来直接使用,避免重复创建。
    • 注意:即使多个线程都用这个static final 的ThreadLocal处理数据也没有问题,因为在每一个Thread内部,都有ThreadLocal.ThreadLocalMap threadLocals;这样一个变量与当前线程绑定,我们get/set/remove时,其实都是对这个threadLocals变量进行的,即使多个线程中 static final ThreadLocal 是一样的,但也是位于不同线程的ThreadLocalMap 中,只是这个ThreadLocalMap 中的key一样,可以使用同一个static final 的ThreadLocal

       

2.4 在spring boot的web项目中使用ThreadLocal一定需要手动remove吗?

       
       基于spring boot创建的web项目中使用ThreadLocal一般都是线程池场景下去使用的,一次controller请求一般都是从Tomcat线程池拿到线程,用完再归还的。除了特殊情况需要动态创建ThreadLocal,绝大部分情况下都是 static final 声明ThreadLocal作为线程间的数据传递来使用的

即使使用 static final 声明 ThreadLocal,也必须在请求结束时通过拦截器或者手动调用 remove(),否则会有以下几点风险

  • 内存泄漏风险
    • 每次请求可能向ThreadLocal放入新数据(例如用户信息),请求结束后,若不remove(),其他请求拿到这个线程继续往里塞数据,value会持续占用内存,导致value堆积最终OOM
  • 数据污染
    • 线程复用时,残留的上次请求数据会被新请求读到,导致严重的安全问题和业务逻辑错误(例如:A用户看到B用户数据)

基于以上问题,在spring boot 项目中使用ThreadLocal需要使用static final 声明 ,在使用完毕后,并通过拦截器或者手动调用 remove()。
       

Spring Boot Web 项目中使用 static final 修饰 ThreadLocal ,此时entry的key就一直被强引用了吗?

  • ​​static final的作用​​:保证ThreadLocal变量是一个静态常量,即该变量所引用的ThreadLocal实例在类加载后就不会改变(不会被重新赋值),且该实例在类卸载前一直存在(由类加载器持有强引用)。
  • ​​Entry中key的引用类型​​:不管ThreadLocal实例被谁引用,只要它作为ThreadLocalMap的key,就会被包装成一个弱引用(Entry extends WeakReference<ThreadLocal<?>>)。所以,即使ThreadLocal实例有强引用(比如static final变量)存在,Entry中key的引用仍然是弱引用。

由于static final持有了强引用,这个ThreadLocal实例将不会被GC回收(只要它的类加载器没有被卸载),所以Entry中的key(弱引用)指向的对象会一直存在(即不会变成null)。因此在ThreadLocalMap中,该Entry的key始终指向那个static final的ThreadLocal实例,不会被回收。​

       

3. 源码分析

       

①:ThreadLocalMap

ThreadLocal内部使用ThreadLocalMap来持有元素,它是ThreadLocal的核心,我们可以把ThreadLocalMap看做是一个定制化的HashMap。它的数据结构如下图所示。
在这里插入图片描述

我们知道HashMap是由数组加链表组成的,但ThreadLocalMap只使用到了数组,并且数组是首尾相连的环形结构,后面会解释原因。
由源码可知,ThreadLocalMap的初始容量为16,负载因子为2/3。

/**
     * 初始容量。必须是2的次方。
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * table数组,必要时会扩容。数组长度必须是2的次方。
     */
    private Entry[] table;

    /**
     * 数组中元素个数
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     * 下次扩容时使用的容量值。
     */
    private int threshold; // Default to 0

    /**
     * Set the resize threshold to maintain at worst a 2/3 load factor.
     * 设置扩容的阈值,以维持负载因子至少为2/3.
     */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }/**
     *  构造器。ThreadLocalMap使用的【懒初始化】,当有多个Entry要存放时,只会先创建一个Entry。
     */
    ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
        //初始化table数组
        table = new Entry[INITIAL_CAPACITY];
        //根据key的hash值定位其在数组中位置
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //在数组中创建首个节点
        table[i] = new Entry(firstKey, firstValue);
        //设置初始化元素个数
        size = 1;
        //设置初始阈值
        setThreshold(INITIAL_CAPACITY);
    }

ThreadLocalMap使用的懒初始化,当有多个Entry节点要存储时,也只能先通过构造器来先初始化一个Entry节点。而ThreadLocalMap初始化的时机是在ThreadLocal中首次调用set或get方法时。
       

②:set操作

       ThreadLocal内部使用ThreadLocalMap来持有元素,它是ThreadLocal的核心,ThreadLocalMap可以看做是一个定制化的HashMap。查看Thread类的源码,可以发现Thread类有一个threaLocals属性,类型为ThreadLocalMap类型,所以可以理解为每个线程都会绑定一个ThreadLocalMap。

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null; //本地变量 
}

存放数据时,我们会调用ThreadLocal.set(value)方法

public void set(T value) {
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap,注意这个是每个线程维持一份的map
        ThreadLocalMap map = getMap(t);
        if (map != null)
        	// 向当前线程中塞对应的value
            map.set(this, value);
        else
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

set方法会先获取当前线程,然后通过getMap获取当前线程绑定的ThreadLocalMap

  • 如果getMap结果非空,则进行保存
  • 如果getMap结果为空,说明是第一次调用set方法,需要先实例化ThreadLocalMap。

如果getMap方法返回为空,说明是第一次调用set方法,需要先实例化ThreadLocalMap,前面已经讲过,不再赘述。

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

如果getMap方法返回不为空,将<ThreadLocal实例,value>作为键值对保存到ThreadLocalMap中。 保存的具体步骤是怎么样的呢,我们继续跟踪。set(value)-->set(key, value)

private void set(ThreadLocal<?> key, Object value) {

        // We don't use a fast path as with get() because it is at
        // least as common to use set() to create new entries as
        // it is to replace existing ones, in which case, a fast
        // path would fail more often than not.

        Entry[] tab = table;
        int len = tab.length;
        //通过hash定位
        int i = key.threadLocalHashCode & (len-1);

        //从当前节点开始,往后线性探测
        for (Entry e = tab[i];//当前节点
             e != null;
             e = tab[i = nextIndex(i, len)]) {//下一个探测节点
            //探测节点的key
            ThreadLocal<?> k = e.get();
            
            //要存入的key与探测节点的key相等,直接覆盖
            if (k == key) {
                e.value = value;
                return;
            }
            //探测节点key为空,说明被回收了【弱引用原因】
            //说明可以使用该位置,用新k-v将其替换。
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        
        //执行到这里,说明探测到了空位置,可以插入
        tab[i] = new Entry(key, value);//创建节点插入
        int sz = ++size;
        
        //插入后需要检测一下容量
        //先尝试启发式清理,如果无法回收且容量又达到阈值,则需要扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

(1)hash定位

       首先,使用hash算法来定位到数组的下标,使用的hash算法与上面讲解初始化时一样,不再赘述。

通过hash算法得到的位置,并不一定就是最终value将要存放的位置。ThreadLocalMap同HashMap一样也存在hash冲突问题。

(2)使用线性探测,解决hash冲突

       我们知道HashMap是使用链地址法来解决hash冲突的,而ThreadLocalMap则是使用的另一种解决hash冲突的方法:开放地址法。所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

开放地址法可以用公式表示为 ( hash(key) + di ) % m,这里di是个变量,表示每次移动的步数。开放地址法在进行探测时,di有下列几种取法:

  • 线性探测再散列:di=1,2,3,……m-1 。这种探测最简单。
  • 二次探测再散列:di=21,-21,22,-22,……k2 (k<2/m)
  • 随机探测再散列:di取伪随机数列

TheadLocalMap使用的是线性探测法,每次探测都是通过nextIndex()往后挪动一步。如果当前已经是最后一个节点,则探测第一个节点。这就说明了ThreadLocalMap中的数组是环形结构。但这里暂时还不能看出它是一个双向的,需要结合后面的prevIndex方法才能确定,后面再讲。

/**
     * Increment i modulo len.
     */
    private static int nextIndex(int i, int len) {
        //下一个节点的位置。如果当前是最后一个节点,则下一个节点是第一个节点。
        return ((i + 1 < len) ? i + 1 : 0);
    }

在探测的过程,对探测的节点进行判断

  • ①如果探测节点的key与要存入的key相等,则直接覆盖。停止探测。
  • ②如果探测节点的key为空,说明由于弱引用的原因被回收了。说明该位置可以重新利用,直接替换掉即可。停止探测。

(3)启发式清理

       如果上面的两种方式无法找到插入点,就只能找空节点来插入了。一旦遇到了空节点,就停止探测,准备在此处插入。插入前先做一次启发式清理操作。 原因就是,ThreadLocalMap同HashMap一样也是有负载因子的,当到达一定容量后就会进行rehash。rehash毕竟是一个耗性能的操作,应该尽量避免。所以,如果先通过启发式清理,能找到已经失效(key=null)的空间可以重复利用,这样就能避免rehash。

private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }

    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // expunge entry at staleSlot
        //清理失效的节点
        tab[staleSlot].value = null; //value设为null
        tab[staleSlot] = null;       //整个Entry设为null
        size--;

        // Rehash until we encounter null
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) {//k为null表示引用的对象已经被gc回收了
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                //hash不相等,说明这个元素之前发生过hash冲突(本应放在这却没放在这),
                //现在因为有元素被移除了,很有可能原来冲突的位置空出来了,重试一次
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;

                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    //继续采用链地址法存放元素
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

(4)rehash

如果启发式清理没能清理出过期空间(key==null),而容量又达到了阈值,就只能rehash了。

private void rehash() {
        //全量清理
        expungeStaleEntries();

        // Use lower threshold for doubling to avoid hysteresis
        if (size >= threshold - threshold / 4)
            //扩容
            resize();
    }
    

    /**
     * 做一次全量清理失效节点的操作
     */
    private void expungeStaleEntries() {
        Entry[] tab = table;
        int len = tab.length;
        for (int j = 0; j < len; j++) {
            Entry e = tab[j];
            if (e != null && e.get() == null)
                expungeStaleEntry(j);
        }
    }
    

    /**
     * Double the capacity of the table.
     */
    private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;//扩容后的容量为原来的2倍。因为要为2的次幂。
        Entry[] newTab = new Entry[newLen];
        int count = 0;

        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null; // Help the GC
                } else {
                    int h = k.threadLocalHashCode & (newLen - 1);
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    newTab[h] = e;
                    count++;
                }
            }
        }

        setThreshold(newLen);
        size = count;
        table = newTab;
    }

       

③:get操作

取数据时,我们会调用ThreadLocal.get()方法

public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程绑定的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        // map为空,则自动为其设置一个初始值并返回。
        return setInitialValue();
    }    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

同样会先获取执行该方法的当前线程,然后再获取该线程绑定的ThreadLocalMap,将当前ThreadLocal实例作为key来获取Map中保存的value值。get()-->getEntry()如下:

/**
     * Get the entry associated with key.
     * This method itself handles only the fast path: a direct hit of existing key.
     * It otherwise relays to getEntryAfterMiss.
     * This is designed to maximize performance for direct hits,in part by making this method readily inlinable.
     *
     * 根据key获取Entry节点。
     */
    private Entry getEntry(ThreadLocal key) {
        //根据key的hash值定位在数组中位置i
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        //找到目标
        if (e != null && e.get() == key)
            return e;
        else
            //找不到,则可能是因为碰撞而存到别处了。
            return getEntryAfterMiss(key, i, e);
    }

    /**
     * getEntry未直接命中的时候调用此方法
     */
    private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        // 向后不断进行线性探测
        while (e != null) {
            ThreadLocal k = e.get();
            //找到目标
            if (k == key)
                return e;
            //key为null,清除失效的Entry
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            //探测下一个
            e = tab[i];
        }
        return null;
    }

       

④:remove操作

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
     
    /**
     * Remove the entry for key.
     */
    private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                //直接将弱引用设为null,断开对对象的引用。
                e.clear();
                //清理无效节点
                expungeStaleEntry(i);
                return;
            }
        }
    }

       

4. 继承性 InheritableThreadLocal

       在一些场景中,子线程需要可以获取父线程的本地变量,比如子线程要获取父线程中保存的用户的信息,或者使用一个统一的traceId来进行链路追踪。但是ThreadLocal不支持继承性,即子线程无法从父线程中获取父线程的本地变量。原因很简单,因为操作ThreadLocal时每次都是获取的当前线程。因此,JDK提供了InheritableThreadLocal来解决继承性问题。

       InheritableThreadLocal 继承了ThreadLocal,并且重写了createMap等三个方法。当首次调用set方法时,创建的是当前线程的inheritableThreadLocals变量,而不再是threadLocals。同样在调用get方法时,获取当前线程的内部map时,获取的是inheritableThreadLocals而不再是threadLocals。总的来说,在InheritableThreadLocal中,变量inheritableThreadLocals替代了threadLocals。

       当父线程创建子线程时,构造函数会将父线程中的inheritableThreadLocals变量里的本地变量赋值到子线程的inheritableThreadLocals里面。

// new Thread()-->init()

init(){
    ……
    //如果父线程inheritableThreadLocals变量不为null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //设置子线程中的inheritableThreadLocals变量
        this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ……
}

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

使用案例:

public class TenantRecordContext {
    public static final InheritableThreadLocal<String> USER_INFO_CONTEXT = new InheritableThreadLocal<>();

    public static void setUserInfo(String sid) {
        USER_INFO_CONTEXT.set(sid);
    }

    public static String getUserInfo() {
        return USER_INFO_CONTEXT.get();
    }

    public static void clearUserInfo() {
        USER_INFO_CONTEXT.remove();
    }
}

       

5. 各类ThreadLocal问题总结

       
1. ThreadLocal的作用?

主要解决线程变量隔离问题。在实际使用中,我们有两种典型用法

  • 用来隔离线程变量。比如在数据库连接池中,可以将数据库连接Connection对象放在ThreadLocal中,每个线程获取各自的数据库连接,线程间不会互相干扰。

  • 用来在同一个线程的调用链中传递数据。

2. ThreadLocal的工作原理?(如何实现线程变量隔离的?)hash冲突解决方法?

  • 工作原理略。每个线程都持有了一个线程本地变量,每个线程只操作自己的线程本地变量,线程间避免了相互干扰。

  • ThreadLocal使用开发地址法中的线性探测来解决hash冲突。

3. ThreadLocalMap为什么要定义在ThreadLocal中,而不是Thread中?

        Thread类有个ThreadlocalMap类型的成员变量,但ThreadlocalMap的定义却在Threadlocal中。将ThreadLocalMap定义在Thread中似乎看起来更符合逻辑,但是实际上并不需要在Thread中操作ThreadLocalMap,定义在Thread类中只会增加一些不必要的开销。 定义在ThreadLocal类中的原因是ThreadLocal类负责ThreadLocalMap的创建,仅当线程中设置第一个ThreadLocal时,才为当前线程创建ThreadLocalMap,之后所有其他ThreadLocal变量将使用一个ThreadLocalMap。
       总的来说就是,ThreadLocalMap不是必需品,定义在Thread中增加了成本,定义在ThreadLocal中按需创建。

4. 既然是线程局部变量,那为什么不用线程(Thread)对象作为key,获取变量时通过线程作为key来获取,这样不是更清晰?

这样设计就存在数据覆盖的问题。如果用线程对象作为key,假设已经存入了用户信息,存入<线程,用户信息>。这时需要新增加用户地理位置信息,需要存入<线程,用户地理信息>。由于key都是同一个线程,不就覆盖了嘛。

5. 那使用ThreadLocal新增信息应该怎么做呢?

为当前线程再绑定一个Threadlocal对象不就好了。比如已经存入了用户信息,要新增加用户的地理信息,如下:

Threadlocal<Geo> geo = new Threadlocal<> ();
geo.set(地理信息);

这样线程的ThreadlocalMap里面就会有二个元素,一个是用户信息,一个是地理位置。(一个线程绑定一个ThreadLocalMap,一个ThreadLocalMap存多个ThreadLocal实例。

6. 如果有多个变量都要塞到ThreadlocalMap中,那岂不是要申明多个Threadlocal 对象?有没有好的解决办法。

  • 可以再封装一下,把这些变量打包成一个Map不就好了,整个Map作为value存入,这样就只需要一个Threadlocal 对象。

7. 为什么ThreadLocalMap中key被设计成弱引用类型?

key设计为弱引用是为了尽最大努力避免内存泄漏,解决的是ThreadLocal对象的内存泄露问题
 
ThreadLocal的设计者考虑到了某些线程的生命周期较长,比如线程池中的线程。由于存在Thread -> ThreadLocalMap -> Entry这样一条强引用链,如果key不设计成弱引用类型,是强引用的话,key就一直不会被GC回收,一直不会是null,Entry就不会被清理。
 
(ThreadLocalMap根据key是否为null来判断是否清理Entry。因为key为null时,引用的ThreadLocal实例不可达会被回收。value又只能通过ThreadLocal的方法来访问,此时相当于value也没用处了。所以,可以根据key是否为null来判断是否清理Entry。)

8. ThreadLocal内存泄露的原因?要如何避免?

弱引用解决的是ThreadLocal对象的内存泄露问题,但value还存在内存泄露的风险。
 
内存泄露的原因:
 

  • 由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经被回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry,导致内存泄漏。

 
ThreadLocal自身采取的措施:
 

  • 但实际上,ThreadLocal内部已经为我们做了一定的防止内存泄漏的工作。ThreadLocalMap提供了一个expungeStaleEntry方法,该方法在每次调用ThreadLocal的get、set、remove方法时都会执行清理工作,即ThreadLocal内部已经帮我们做了对key为null的Entry的清理工作:擦除Entry(置为null),同时检测整个Entry数组将key为null的Entry一并擦除,然后重新调整索引。

 
但是必须需要调用这三个方法才会触发清理,很可能我们使用完之后就不再做任何操作了(set/get/remove),这样就不会触发内部的清理工作。

开发人员需要注意: 所以,通常建议每次使用完ThreadLocal后,立即调用remove方法

9. 为什么使用ThreadLocal时通常定义为static?

ThreadLocal 对象建议使用 static 修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享
此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只 要是这个线程内定义的)都可以操控这个变量。

10. ThreadLocal继承性问题?如何解决?

ThreadLocal不支持子线程继承,可以使用JDK中的InheritableThreadLocal来解决继承性问题。对于线程池等场景,可以使用淘宝技术部哲良实现的TransmittableThreadLocal

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值