【ThreadLocal 与 InheritableThreadLocal 源码解析】:数据存取 | 线程属性继承

前言(关于源码航行)
在这里插入图片描述

在准备面试和学习的过程中,我阅读了还算多的源码,比如 JUC、Spring、MyBatis,收获了很多代码的设计思想,也对平时调用的 API 有了更深入的理解;但过多散乱的笔记给我的整理复习带来了比较大的麻烦。
📋 在 C 站零零散散发了 JUC 的源码解析和集合源码解析,收到了很多朋友的喜爱,这里我准备将一些源码解析的文章整合起来,为了方便阅读和归纳在这里整合成目录:源码航行阅读目录,大家感兴趣的话可以关注一下!

ThreadLocal 源码解析

ThreadLocal ,从字面含义上来看就是:“线程本地”,这形象的说明了它的用处: ThreadLocal 正是 Java 提供的一种线程本地存储机制,可以利用该就只将数据还存在某个线程的内部,该线程可以在任意的时刻、任意的方法中去获取缓存的数据。

基本介绍

ThreadLocal 共有两种类型, ThreadLocal 本身和他的子类 InheritableThreadLocal

普通类型的 ThreadLocal: 这种 ThreadLocal 可以存储任意类型的数据,例如:

ThreadLocal<String> threadLocalString = new ThreadLocal<>();
ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<>()

普通类型的 ThreadLocal 可以用于在每个线程中存储不同类型的数据,并且每个线程访问该ThreadLocal 对象时,都可以获取到其线程私有的变量副本。

InheritableThreadLocal 类型的 ThreadLocalInheritableThreadLocalThreadLocal 的一个子类,允许子线程访问父线程设置的本地变量。当子线程创建时,它会继承父线程中 InheritableThreadLocal 的值。例如:

InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

ThreadLocal 底层是通过 ThreadLocalMap 实现的,每个 Thread 对象(注意不是 ThreadLocal 对象,这些数据都是 Thread 对象来引用的)中都有一个 ThreadLocalMap,Map 中的 Key 为 ThreadLocal 对象,Map 的 value 为缓存需要的值。

	/*
	ThreadLocal values pertaining to this thread. This map is maintained
	 * by the ThreadLocal class.
	 */
	ThreadLocal.ThreadLocalMap threadLocals = null;

通过下面的图可以很清晰的看出线程和 ThreadLocal 的对应关系

在这里插入图片描述

ThreadLocal 篇

ThreadLocal 的 set 方法

下面我们通过 debug 这段代码,来看看 ThreadLocal 的执行逻辑:

public static void main(String[] args) {
    threadLocal.set("Hello World!");
    System.out.println(threadLocal.get());
}

首先调用的是 void ThreadLocal.set(T value)

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

先通过 native 本地方法获取到当前请求的线程,然后将获得的线程作为参数传递给 getMap(Thread t) 方法,这个方法就是用来获取 Thread 对象的 ThreadLocalMap 属性:

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

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

threadLocals 的初始值为空,它正式的创建时机就是第一次调用 ThreadLocalset() 方法的时候,所以可以看到上面还有一个 createMap 方法;如果是第二次添加,此时的 map 不为空,就直接通过调用 mapset 方法来添加一个新的键值对。

这里要特别关注键值对的内容:键是 this 也就是 ThreadLocal 对象本身,而值就是上面传入的 value,从这里可以看出,ThreadLocal 是作为键值对的键存在的,真实的数据其实就是存储在线程对象 Thread 中的。


真正负责存储的数据结构-ThreadLocalMap

既然谈到了 ThreadLocalMap 类,这里来详细的看看它是如何实现键值对的对应关系以及是如何在 Entry 通过虚引用来防止内存泄漏的:

static class ThreadLocalMap {}

ThreadLocalMapThreadLocal 的一个静态内部类,它并没有继承任何的父类,而是在内部自己实现了一个 Map

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
        
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

真实的数据就是存储在 Entry 对象中,这个对象除了存储 value 对象以外,还继承了 WeakReference 虚引用,用来引用 ThreadLocal 对象,可以通过 Entry.get() 方法来获取虚引用指向的对象,当出现调用 Map 的 get 方法出现了哈西冲突的时候,可以通过 Entry.get().threadLocalHashCode 来获取到哈希值,与当前传入方法的 key 做一个比较,就能确定当前索引下的对象是不是所需要的了。

而当对象仅被虚引用所指向的时候,JVM 进行 GC 垃圾回收的时候,会直接将其回收, ThreadLocal<?> t = new ThreadLocal<>() 这样的指向方式是一种强应用,当这个引用失效的时候,也就是 ThreadLocal 对象彻底无法抵达的时候, Entry 对象中的虚引用并不会影响 GC 的回收,此时就避免了内存泄露的问题。

既然要实现一个 Map,那就绕不开 key 的映射问题以及如何解决哈希冲突了, ThreadLocalMap 的映射方式比较简单:

int i = key.threadLocalHashCode & (len-1);

就是直接将 ThreadLocal 对象的 HashCode 与数组的长度减一做一个按位与的操作,将其映射到数组中的一个位置。

ThreadLocalMap 中,采用了线性探测的方式来解决哈西冲突,当出现了哈西冲突的时候,会调用 nextIndex 方法来获取下一个存放位置:

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

就是在原位置的基础上不断加一,当越界的时候就返回下标为 0 的位置继续寻找,是一种简单的线性探测的方式。

关于 ThreadLocalMap 中的具体方法这里就不过多赘述了,之后有机会可以出一篇单独的内容。


ThreadLocal 的 get 方法

看完了上面的 set 方法,猜一下也能知道 get 方法就是根据 ThreadLocal 对象来从线程对象ThreadLocalMap 中去取得对应的值:

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

首先通过 getMap() 方法获取线程对象的 ThreadLocalMap ,然后通过 getEntry() 方法从 map 中获取对应的数据,也就是通过 ThreadLocal 的哈希值,去映射到ThreadLocalMap 的 Entry 数组的一个下标,来将对应的对象取出。

在线程池中,ThreadLocal 为什么会引发内存问题?

在线程池中使用 ThreadLocal 可能会引发内存问题,主要是因为线程池中的线程是复用的,而 ThreadLocal 变量的设计初衷是每个线程拥有一个独立的副本。当线程执行完任务后,如果没有正确地清理 ThreadLocal 中的数据,这些数据就会保留在内存中,从而引起内存泄漏。

  1. 生命周期差异ThreadLocal变量的生命周期理论上与创建它的线程相同,但在线程池中,线程的生命周期远远长于单个任务的生命周期。如果任务没有明确地清除ThreadLocal 变量的内容,那么这些变量会持续占用内存,直到线程池被销毁或JVM退出。
  2. 垃圾回收障碍:当 ThreadLocal 变量引用了任务相关的数据,而这些数据又引用了其他对象时,即使任务已经完成,只要线程还存活,这些对象就不会被垃圾回收,因为从根节点(线程)到这些对象的引用路径依然存在。
  3. 静态ThreadLocal变量:如果ThreadLocal是静态的,那么它在整个应用程序的生命周期内都会存在,即使线程已经不再使用它。这可能导致大量无用数据长期驻留内存。

为了避免这些问题,可以采取以下措施:

  • 显式清理:在任务结束时,显式地调用ThreadLocalremove()方法来清理不再需要的数据。这可以断开ThreadLocal变量和任务数据之间的引用,使垃圾回收器能够回收不再使用的对象。
  • 合理设计:尽量减少ThreadLocal变量的使用,只在确实需要线程隔离的状态时使用,并确保每个线程的任务结束后能够安全地释放这些变量所持有的资源。

除了上面的措施,其实还有最后一种优化的方式,它适用于子线程需要继承父线程的 ThreadLocal 对象的时候:也就是我们前面提到的 InheritableThreadLcoal —— 可继承的 ThreadLocal

InheritableThreadLocal 篇

使用InheritableThreadLocal并不能直接避免ThreadLocalMap的内存泄漏问题,但它提供了一个额外的机制,可以配合正确的使用和清理策略来帮助减少内存泄漏的风险,尤其是在线程池和多线程环境下。ThreadLocalInheritableThreadLocal都会在线程的ThreadLocalMap中存储数据,而内存泄漏的风险主要来源于线程长时间存活时ThreadLocal变量未被清理,导致ThreadLocalMap中积累了大量不再需要的条目。在线程池中,线程往往复用多次,这种情况下如果没有适当的清理策略,ThreadLocalMap中的数据会逐渐累积,导致内存泄漏。

继承关系:
在这里插入图片描述

InheritableThreadLocal 相较于直接赋值有什么好处?

InheritableThreadLocal本身的设计目的是允许子线程继承父线程的ThreadLocal值。它和ThreadLocal一样,需要在不再需要时显式调用remove()方法来清理。然而,InheritableThreadLocal提供了几个特性,可以帮助更好地管理线程间的数据传递和清理:

  1. 自动清理:当一个新线程启动并继承了父线程的InheritableThreadLocal值时,这些值会被复制到新线程的ThreadLocalMap中,相当于是一个深层次的赋值,而不是仅仅存储原来的引用;这意味着父线程的InheritableThreadLocal值不再需要被子线程引用,从而减少了父线程的ThreadLocalMap的引用关系,有助于垃圾回收。
  2. 生命周期管理:虽然InheritableThreadLocal本身不会自动清除值,但它的设计鼓励了更细粒度的线程生命周期管理。例如,在Web应用服务器中,每个HTTP请求可能在单独的线程中处理,使用InheritableThreadLocal可以确保每个请求的上下文数据在请求结束后被正确清理,从而避免了数据在ThreadLocalMap中的累积。
  3. 可配置的清理策略:在某些框架和应用中,可以配置特定的清理逻辑,例如在Servlet容器中,可以在请求结束时自动调用InheritableThreadLocalremove()方法,以确保每个请求的线程在重用前被清理干净。

因此,虽然InheritableThreadLocal本身并不能直接防止ThreadLocalMap的内存泄漏,但结合正确的清理策略和生命周期管理,它可以作为整体解决方案的一部分,帮助减少内存泄漏的风险。在实践中,无论是使用ThreadLocal还是InheritableThreadLocal,都应该遵循良好的编码实践,如在适当的时机调用remove()方法,以确保不再需要的数据能够被及时释放。

InheritableThreadLocal 是如何实现继承的?

InheritableThreadLocal 继承了父类 ThreadLocal 并且重写了它的 getMap()createMap() 等方法

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
    
	  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

当创建和获取的时候,都使用的是 ThreadLocal 中的 inheritableThreadLocals 属性,关于继承和使用的逻辑,也都是围绕这个属性去实现的,我们跟着下面的调试代码来具体的看一下:

public class Main {

    static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    public static void main(String[] args) {
        Thread parentThread = new Thread(() -> {
            threadLocal.set("Hello World");
            Thread sonThread = new Thread(() -> {
                System.out.println(threadLocal.get());
            });
            sonThread.start();
        });
        threadLocal.remove();
        parentThread.start();
    }
}

当调用父线程调用 threadLocal.set() 方法的时候,调用的还是 ThreadLocal 中的 set() 方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

只是此时通过 getMap() 获取到的不再是线程对象的 threadLocals 而是 inheritableThreadLocals 属性。

通过上面的内容,我们知道了在线程对象中,InheritableThreadLocal 的值和 ThreadLocal存储在不同属性中的;当子线程创建的时候,就会去检测父线程中的InheritableThreadLocal 属性中是否存在值,如果存在会将其复制一份,并且保存在自己对应的属性中:

下面展示的就是创建线程的方法,我们掠过和本节内容关系不大的部分:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        // 。。。。。。

        Thread parent = currentThread();
			  
			  // 。。。。。。

        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInhritedMap(parent.inheritableThreadLocals);
				
			  // 。。。。。。
    }
    
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

当判断父线程中的 inheritableThreadLocals 存在值的时候,会调用 ThreadLocal 中的 createInhritedMap 方法;这个方法是静态内部类 ThreadLocalMapprivate 构造方法,它负责将参数中的 ThreadLocalMap 复制一份到当前创建的对象之中:

        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        // 是复制一份,而不是直接存储
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
        
		    // 控制子线程的继承内容  
		    protected T childValue(T parentValue) {
		        return parentValue;
		    }

通过上面的 Entry c = new Entry(key, value); 就可以看出,是将 Entry 直接复制了一份,而不是单纯的指向。

这样,子线程就继承了父亲线程的的 InheritableThreadLocal 中存储的内容。

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

*Soo_Young*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值