ThreadLocal取不到值的两种原因

本文通过一个示例详细解释了ThreadLocal在不同类加载器下导致无法共享数据的问题,指出当两个类加载器加载的类引用了相同的静态ThreadLocal变量时,由于它们实际上是不同的对象,因此在同一线程中也无法获取到期望的值。这在依赖注入场景中尤其需要注意,避免手动注入与容器创建的依赖使用不同类加载器的情况。

1.两种原因

  • 第一种,也是最常见的一种,就是多个线程使用ThreadLocal
  • 第二种,类加载器不同造成取不到值,本质原因就是不同类加载器造成多个ThreadLocal对象
public class StaticClassLoaderTest {
    protected static final ThreadLocal<Object> local = new ThreadLocal<Object>();
    //cusLoader加载器加载的对象
    private Test3 test3;

    public StaticClassLoaderTest() {
        try {
            test3 = (Test3) Class.forName("gittest.Test3", true, new cusLoader()).newInstance();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    public Test3 getTest3() {
        return test3;
    }
    public static void main(String[] args) {
        try {
            //默认类加载器加载StaticClassLoaderTest,并设置值
            StaticClassLoaderTest.local.set(new Object());
            new StaticClassLoaderTest().getTest3();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    //自定义类加载器
    public static class cusLoader extends ClassLoader {
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (name.contains("StaticClassLoaderTest")) {
                InputStream is = Thread.currentThread().getContextClassLoader()
                        .getResourceAsStream(name.replace(".", "/") + ".class");
                ByteArrayOutputStream output = new ByteArrayOutputStream();
                try {
                    IOUtils.copy(is, output);
                    return defineClass(output.toByteArray(), 0, output.toByteArray().length);
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return super.loadClass(name, resolve);
        }
    }

}
public class Test3 {

    public void test() {
        //由cusLoader加载器加载StaticClassLoaderTest,并获取值,由于StaticClassLoaderTest并不相同所以无法获取到值
        System.out.println(StaticClassLoaderTest.local.get());
    }
}

2.总结

  • 2个类加器加载的对象引用了相同的静态变量ThreadLocal,实际上ThreadLocal并不是同一个值,所以即使在一个线程中也获取不到期望的值。
  • 像依赖注入,如果你自己创建了一个对象,然后用手动注入了一个容器创建的依赖,假设这个依赖是自定义类加器创建的,可能会造成这种情况。
### ThreadLocal 造成内存泄漏的原因 `ThreadLocal` 导致内存泄漏的核心原因与其内部工作机制密切相关。在 Java 中,`ThreadLocal` 维护了一个以 `Thread` 为键、以用户设置的的映射表,该映射存储在每个 `Thread` 对象的一个名为 `ThreadLocalMap` 的字段中[^2]。`ThreadLocalMap` 是一个自定义的数据结构,它的每一个条目(Entry)由两部分组成:一个是作为键的 `ThreadLocal` 实例(弱引用),另一个是作为的实际数据(强引用)。当某个 `ThreadLocal` 被外部销毁后,如果没有及时清理对应的 `ThreadLocalMap` 条目,就会导致以下两种潜在问题: 1. **Key 弱引用失效**:虽然 `ThreadLocal` 的 Key 使用的是弱引用,这意味着一旦没有任何其他强引用指向这个 `ThreadLocal` 实例时,GC 可以回收它。然而,如果 Value 数据仍然存在且被强引用保留,则 Entry 自身不会立即从 Map 中移除,进而形成悬挂的条目[^3]。 2. **Value 强引用持续存在**:由于 Value 部分采用强引用的方式保存着实际对象实例,只要这些对象没有被显式清除掉,即便关联的 `ThreadLocal` 已经不可达,那些大容量或长期存在的对象仍会占据大量堆空间,最终引发内存泄漏风险。 --- ### 解决方案 为了避免上述情况的发生,以下是几种常见的解决策略: #### 1. 明确调用 `remove()` 方法 最直接有效的手段是在不再需要某特定 `ThreadLocal` 存储的内容之后立刻调用其 `remove()` 函数来彻底删除相应的映射关系[^1]。这样做不仅可以使原本属于 Key 的那个 `ThreadLocal` 更早进入可回收状态,同时也切断了对 Value 所代表的那个复杂对象图谱的一切联系,从而让整个链条上的所有废弃组件都能够顺利参与垃圾收集过程。 ```java threadLocalVariable.set(someObject); // 设置初始 try { // 进行业务处理... } finally { threadLocalVariable.remove(); // 清理资源 } ``` #### 2. 利用线程池特性管理生命周期 对于像线程池这样重用固定数量工作线程的应用场景而言,特别要注意每次任务结束前都要主动清空与之相关的全部临时性的 `ThreadLocal` 数据项。这是因为线程池中的线程通常会长时间处于等待新任务到来的状态而不是简单地终止退出;如果不做额外控制的话,之前累积下来的各类中间结果很可能会一直驻留在内存之中得不到释放。 #### 3. 设计合理的默认构造函数 开发者还可以考虑重新定义自己的扩展版 `ThreadLocal` 类型,在其中覆盖原有的 `initialValue()` 方法以便提供更加安全可控的行为模式。例如,可以在适当时候自动触发某些预设好的回调动作去尝试简化甚至完全规避人为失误带来的隐患。 --- ### 总结 综上所述,尽管 `ThreadLocal` 提供了一种便捷的方式来实现线程级别的变量隔离,但如果对其底层原理缺乏足够的理解则很容易陷入诸如内存泄漏之类的陷阱当中。因此,在日常开发过程中应当养成良好的编程习惯——即始终记得适时地清理无用的 `ThreadLocal` 数据,并结合具体的业务需求合理规划相关的设计架构。 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值