ThreadLocal部分核心源码回顾

 

ThreadLocal的API很少就包含了4个,分别是get()、set()、remove()、withInitial(),源码如下:

public T get() {}
 
public void set(T value){}
 
public void remove(){}
 
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        
}
get():从当前线程的 ThreadLocalMap 获取与当前 ThreadLocal 对象对应的值。如果 ThreadLocalMap 中不存在该值,则调用 setInitialValue() 方法进行初始化。
set(T value):将当前线程的 ThreadLocalMap 中的值设置为给定的 value。如果当前线程没有 ThreadLocalMap,则会创建一个新的 ThreadLocalMap 并将值设置进去。
remove():从当前线程的 ThreadLocalMap 中移除与当前 ThreadLocal 对象对应的值,帮助防止内存泄漏。
withInitial(Supplier<? extends T> supplier):返回一个新的 ThreadLocal 对象,其初始值由 Supplier 提供。这允许使用者在创建 ThreadLocal 时指定初始值。
针对这几个源码我们重点进行分析和体会。

ThreadLocal.set()方法源码详解
pubic void set(T value) {
    // 获取当前线程
    Thread t = Threac.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果map不为null, 调用ThreadLocalMap.set()方法设置值
    if (map != null)
        map.set(this, value);
    else 
        // map为null,调用createMap()方法初始化创建map
        createMap(t, value);
}
 
// 返回线程的ThreadLocalMap.threadLocals
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
 
// 调用ThreadLocalMap构造方法创建ThreadLocalMap
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
 
// ThreadLocalMap构造方法,传入firstKey, firstValue
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化Entry表的容量 = 16
    table = new Entry[INITIAL_CAPACITY];
    // 获取ThreadLocal的hashCode值与运算得到数组下标
    int i = firsetKey.threadLocalHashCode & (INITAL_CAPACITY - 1);
    // 通过下标Entry表赋值
    table[i] = new Entry(firstKey, firstValue);
    // Entry表存储元素数量初始化为1
    size = 1;
    // 设置Entry表扩容阙值 默认为 len * 2 / 3
    setThreshold(INITIAL_CAPACITY);
}
 
private void setThreshold(int len) {
    threshold = len * 2 / 3
}
ThreadLocal.set()方法还是很简单的,核心方法在ThreadLocalMap.set()方法

基本流程可总结如下:

ThreadLocalMap.get()方法详解
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;
        }
    }
    // 未找到的话,则调用setInitialValue()方法设置null
    return setInitialValue();
}
 
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // key相等直接返回
    if (e != null && e.get() == key)
        return e;
    else
        // key不相等调用getEntryAfterMiss()方法
        return getEntryAfterMiss(key, i, e);
}
 
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 迭代往后查找key相等的entry
    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;
}
主要包含两种情况,一种是hash计算出下标,该下标对应的Entry.key和我们传入的key相等的情况,另外一种就是不相等的情况。

相等情况:相等情况处理很简单,直接返回value,如下图,比如get(ThreadLocal1)计算下标为4,且4存在Entry,且key相等,则直接返回value = 11:

不相等情况:不相等情况,以get(ThreadLocal2)为例计算下标为4,且4存在Entry,但key相等,这个时候则为往后迭代寻找key相等的元素,如果寻找过程中发现了有key = null的元素则回进行探测式清理操作。如下图:

迭代到index=5的数据时,此时Entry.key=null,触发一次探测式数据回收操作,执行expungeStaleEntry()方法,执行完后,index 5、8的数据都会被回收,而index 6、7的数据都会前移,此时继续往后迭代,到index = 6的时候即找到了key值相等的Entry数据,如下图:

ThreadLocal.remove()方法源码详解
public void remove() {
    // 获取当前线程的 ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 如果当前线程有 ThreadLocalMap,则在 map 中移除当前 ThreadLocal 的值
        m.remove(this);
}
 
static class ThreadLocalMap {
 
    // 内部 Entry 类,继承自 WeakReference<ThreadLocal<?>>
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // ThreadLocal 对应的值
        Object value;
 
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
 
    // 线程局部变量哈希表
    private Entry[] table;
 
    private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;
        int len = tab.length;
        // 计算当前 ThreadLocal 的哈希值在数组中的索引位置
        int i = key.threadLocalHashCode & (len - 1);
        
        // 从hash获取的下标开始,寻找key相等的entry元素清除
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                e.clear();  // 清除键的引用
                expungeStaleEntry(i);  // 清除相应的值
                return;
            }
        }
    }
 
    // 用于计算下一个索引位置
    private int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
 
    // 清除无效的 Entry
    private void expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
 
        // 清除给定槽位的 Entry
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
 
        // 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) {
                e.value = null;
                tab[i] = null;
            } else {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;
 
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
    }
}
ThreadLocal.remove()核心是调用ThreadLocalMap.remove()方法,流程如下:

通过hash计算下标。
从散列表该下标开始往后查key相等的元素,如果找到则做清除操作,引用置为null,GC的时候key就会置为null,然后执行探测式清理处理。

————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/xiaofeng10330111/article/details/139667074

<think>好的,我现在需要帮助用户理解ThreadLocal导致的内存泄漏问题,并且要图文解析,简单易懂。首先,我应该回顾一下ThreadLocal的基本机制。ThreadLocal通过在每个线程中维护一个ThreadLocalMap来存储变量副本,这样每个线程都可以独立访问自己的副本,避免线程安全问题。 接下来,用户提到内存泄漏,这可能涉及到ThreadLocalMap中的Entry。我记得Entry是使用弱引用的ThreadLocal作为键,但值仍然是强引用。如果ThreadLocal对象被回收,而线程仍然存活,那么键会变成null,但值还存在,导致无法访问这些值,从而造成内存泄漏。需要详细解释这个过程,可能需要画图说明。 然后,要说明为什么使用弱引用。弱引用允许在垃圾回收时回收键对象,防止因为线程长时间运行而导致ThreadLocal无法被回收。但如果线程池中的线程长期存在,比如在服务器环境中,这些value对象可能一直不会被回收,导致泄漏。 解决方法方面,用户可能知道需要调用remove方法,但需要强调在不再需要ThreadLocal变量时主动调用remove,尤其是在使用线程池的情况下,因为线程可能会被重用,旧的value可能残留。此外,可能还要提到在JDK的处理中,set和get方法会清理失效的Entry,但这并不完全可靠,所以最好手动remove。 关于图解,可以分步骤展示:正常情况下的ThreadLocalMap结构,ThreadLocal被回收后的Entry状态,以及未清理的value导致的内存泄漏。可能用简单的方框图表示键和值的引用关系,以及GC后的变化。 需要确保语言简单,避免技术术语过多,用比喻或例子帮助理解。比如,把ThreadLocalMap比作一个保险箱,弱引用的钥匙容易被回收,但里面的物品(value)还留在里面,需要主动清理。 最后,检查是否有引用相关的资料,比如用户提供的引用[1]中提到了ThreadLocal的实现机制,可以引用说明源码层面的细节,比如Entry的弱引用设计。引用其他可能相关的部分,但用户给出的引用似乎更多涉及Android UI刷新和Java基础,可能引用较少,不过引用[1]已经足够支持ThreadLocal的实现分析。</think>### ThreadLocal 内存泄漏原理及解决方法 #### 1. 核心原理与内存泄漏成因 ThreadLocal 通过每个线程内部的 `ThreadLocalMap` 存储变量副本。`ThreadLocalMap` 中的 **Entry 使用弱引用(WeakReference)指向 ThreadLocal 对象作为键**,而值(Value)是强引用。 内存泄漏场景: - 当 `ThreadLocal` 对象被回收(例如置为 `null``)后,Entry 的键(Key)变为 `null`,但值(Value)仍然被强引用,导致无法被垃圾回收[^1]。 - 若线程长期存活(如线程池中的线程),这些无效 Entry 会持续占用内存,最终引发内存泄漏。 #### 2. 图解分析 ![ThreadLocal内存泄漏示意图](https://via.placeholder.com/600x300.png?text=ThreadLocal内存泄漏示意图) 1. **正常状态**:Entry 的键(弱引用)指向 ThreadLocal 对象,值(强引用)存储数据。 2. **ThreadLocal 被回收**:键变为 `null`,但值仍存在强引用链:`Thread -> ThreadLocalMap -> Entry -> Value`。 3. **内存泄漏**:无效 Entry 未被清理,导致 Value 无法释放。 #### 3. 解决方法 - **主动调用 `remove()`**:使用完 ThreadLocal 后,调用其 `remove()` 方法清除当前线程的 Entry。 - **避免长生命周期线程**:在池化线程场景中,务必在任务结束时清理 ThreadLocal。 - **JDK 自清理机制**:`set()`/`get()` 方法会触发清理失效 Entry,但无法完全依赖。 #### 4. 代码示例与对比 ```java // 错误示例:未清理导致泄漏 ThreadLocal<User> userThreadLocal = new ThreadLocal<>(); userThreadLocal.set(new User("Alice")); // 使用后未调用 remove() // 正确示例:显式清理 try { userThreadLocal.set(new User("Bob")); // 业务逻辑 } finally { userThreadLocal.remove(); // 强制清除 } ```` ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值