ThreadLocal没用好的例子(构造用户上下文)

本文分析了在使用ThreadLocal存储用户上下文信息时未正确清理导致的问题,并通过修改代码解决了潜在的内存泄漏风险。

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

问题:

讲述一个以前遇到的问题,问题的现象是这样的,通过CRM操作我们接口时因为没有登录,是不会有用户上下文信息的,但是通过日志发现也打印了上下文信息,造成这种情况可能是我们自己用户登录自己的app然后上下文中保存了在了threadlocal中,然后没有释放,因为tomcat线程池的原因,导致线程复用,crm操作时在复用了这个线程就导致打印出来了上下文信息。

查找了一番引用的基础依赖,发现了在内部starter基础依赖中有个filter,filter里会从header头中解析获取用户上线文信息,然后设置到ContextHolder里,ContextHolder里可以看作是保存了一个ThreadLocal变量(里面采用策略模式,将保存Context动作抽象出来了,默认是以ThreadLocal存储Context的策略),这样在每个线程中就可以通过ContextHolder获取到用户上线文。但是这个filter里竟然没有用完的时候清空ContextHolder里的threadlocal(在filterChain.doFilter后没有删除操作)。

有问题(简化后)的结构

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 自己简化的操作 仅供展示
        String user = AttributeHelp.getHeader(Xheader.X_MAN, request, null);
        if (user != null) {
            user = new String(user.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            JSONObject cu = JSONObject.parseObject(user);
            ContextHolder.getContext().setAuthentication(new ContextUser().setUser(cu).setId(cu.getLong("id")).setOrgId(cu.getLong("orgId"))
                    .setUsername(cu.getString("username")).setRealName(cu.getString("realName")).setAccountType(cu.getInteger("accountType")));
        }
        filterChain.doFilter(request, response);
    }

 修改后如下

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 自己简化的操作 仅供展示
        String user = AttributeHelp.getHeader(Xheader.X_MAN, request, null);
        if (user != null) {
            user = new String(user.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            JSONObject cu = JSONObject.parseObject(user);
            ContextHolder.getContext().setAuthentication(new ContextUser().setUser(cu).setId(cu.getLong("id")).setOrgId(cu.getLong("orgId"))
                    .setUsername(cu.getString("username")).setRealName(cu.getString("realName")).setAccountType(cu.getInteger("accountType")));
        }
        filterChain.doFilter(request, response);
        // 用完清除线程的threadlocal
        ContextHolder.clearContext();
    }

 在filterChain.doFilter后添加一个清空threadlocal的操作就完事。。用完就删除

都在讲threadlocal,用完就清空,不清空就会造成内存泄漏,这个虽然也造成了内存泄漏,但是因为数量很少,tomcat线程数默认也就10~200个不会造成很大内存占用,而且如果都是自己的app登录的话都是有上下文的,线程内的上下文信息也会一直的变更也无所谓,但是也要养成好的习惯,用完就删除,万一造成了内存泄漏导致系统崩溃就gg喽~

ThreadLocal原理

介绍一下ThreadLocal原理,增加理解。

Thread

每个Thread对象中都保存着一个ThreadLocal.ThreadLocalMap类型的变量threadLocals,调用ThreadLocal实例的set方法时会将ThreadLocal这个实例,和set入参当作键值对存储到当前调用线程的threadLocals中。

ThreadLocalMap

类似于map,存储Threadlocal实例作为弱引用key,object对象作为value。key是弱引用,弱引用是一旦发生垃圾收集行为,不管内存够不够,都会进行收集,也就是说GC后,key如果没有其他变量的强引用,key就消失了,但是value是强引用,一直在内存中。

ThreadLocal

几个重要的方法

public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取线程Thread的threadLocals变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 获取Map中的entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果没有设置初始值null,返回
        return setInitialValue();
    }
public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取线程Thread的treadLocals变量
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 设置值,this代表threadLocal,里面会用this,和value构造key,value的Entry实体
            map.set(this, value);
        else
            createMap(t, value);
    }
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

弱(WeakReference)引用实验

对照实验1:确定剩余没有被清空的数量,因为是强引用GC的话也不会清理掉arr数据的内容,看到第一次gc有15195k保留

public static void main(String[] args) {
        // vm参数 -verbose:gc,可以控制台看到gc内容
        int[] arr = new int[1024*60*60];
        System.gc();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

对照实验2:增加weakReference引用没有对结果有太大的影响,因为数组实例还是被arr对象引用强引用连接着。

public static void main(String[] args) {
        // vm参数 -verbose:gc,可以控制台看到gc内容
        int[] arr = new int[1024*60*60];
        WeakReference weakReference = new WeakReference(arr);
        System.gc();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

 

对照实验3:因为在作用范围内将arr对象引用断开,数组实例是被weakReference弱引用连着,gc的时候数组实例对象被回收。

public static void main(String[] args) {
        // vm参数 -verbose:gc,可以控制台看到gc内容
        int[] arr = new int[1024*60*60];
        WeakReference weakReference = new WeakReference(arr);
        arr = null;
        System.gc();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

 

模拟ThreadLocalMap中entry的实验

MyEntry模拟ThreadLocalMap中的Entry,MyEntry采用int数组代替threadlocal,被gc清除的时候可以清楚看到内存变化。

static class MyEntry extends WeakReference<int[]> {
        private int[] arrValue;
        public MyEntry(int[] key, int[] arrValue) {
            super(key);
            this.arrValue = arrValue;
        }
    }

对照实验一:

public static void main(String[] args) {
        // vm参数 -verbose:gc,可以控制台看到gc内容
        // arr 代替Threadlocal,被gc清除时可以清楚看到内存变化
        int[] arr = new int[1024*60*60];
        int[] arrValue = new int[1024*60*60];
        MyEntry myEntry = new MyEntry(arr, arrValue);
        System.gc();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

因为arr和arrValue都保留着强引用,所以gc的时候不会清除掉 

对照实验二:

将arr和arrValue设置为null,断开强引用。

public static void main(String[] args) {
        // vm参数 -verbose:gc,可以控制台看到gc内容
        // arr 代替Threadlocal,被gc清除时可以清楚看到内存变化
        int[] arr = new int[1024*60*60];
        int[] arrValue = new int[1024*60*60];
        MyEntry myEntry = new MyEntry(arr, arrValue);
        arr = null;
        arrValue = null;
        System.gc();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

结果显示MyEntry的key被回收掉了。

 通过模拟entry的对照实验,可以发现entry里key时弱引用,gc时会回收,value是强引用,不会被回收掉。这也是一些threadlocal没有remove导致内存泄漏的原罪。

用一副内存结构图来表示一下ThreadLocal

 

结论:

1.ThreadLocal用完要remove清理掉,防止出现其他问题。

2.WeakReferene<T>是弱引用,如果WeakReferene<T>引用的实例没有其他对象引用连接,GC的时候会被清理掉,只清理范型的那个值。

 

 

### Java 中 `ThreadLocal` 的使用场景及原理 #### 使用场景 在多线程环境中,某些资源或状态需要在线程之间保持独立。为了实现这一点而不必频繁创建对象实例,Java 提供了 `ThreadLocal` 类。通过这种方式可以在每个线程内维护一份独立的副本,从而避免多个线程之间的干扰。 具体来说,在以下几种情况下适合采用 `ThreadLocal`: - **数据库连接**:当应用程序中的不同部分都需要访问同一个数据库时,可以利用 `ThreadLocal` 来保存每个线程对应的数据库连接。 - **事务管理器**:类似于数据库连接的例子,如果应用中有分布式事务的需求,则可以用它来存储每条执行路径上的事务上下文信息。 - **用户会话跟踪**:Web 应用程序通常会在整个请求处理过程中保留用户的登录凭证或其他个性化设置;此时借助此机制能够简化跨层间的数据传递过程[^3]。 #### 原理分析 实际上,`ThreadLocal` 并不是真正意义上的“线程局部”,而是基于内部的一个名为 `ThreadLocalMap` 的哈希表结构工作。每当调用 `set()` 方法向某个特定类型的 `ThreadLocal` 设置值的时候,实际操作的是当前运行着该代码片段的那个线程所持有的私有映射关系——即 `ThreadLocalMap` 实例。因此即使两个不同的类定义了自己的静态成员变量指向同一份 `ThreadLocal<T>` 对象,只要它们分别位于各自的线程之中就不会相互影响[^2]。 下面给出一段简单的例子说明如何初始化并读取一个整型数值级别的 `ThreadLocal<Integer>` 变量: ```java public class ThreadLocalTest { public static void main(String[] args) throws InterruptedException { // 创建一个新的ThreadLocal实例 ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); // 给主线程关联上初始值6 threadLocal.set(6); System.out.println("父线程获取数据:" + threadLocal.get()); // 启动新线程尝试打印其自己的threadlocal值,默认为空(null) Thread t = new Thread(() -> { Integer val = threadLocal.get(); System.out.println("子线程获取数据:" + (val != null ? val : "null")); }); t.start(); // 等待子线程结束再继续往下走 t.join(); } } ``` 这段代码展示了在一个新的线程启动之前给定了一次显式的赋值动作之后,只有原来的那个设置了值得地方才能看到这个设定的结果,而新开辟出来的那一条分支里边并没有继承到任何东西,所以输出结果将是 `"null"`[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值