ThreadLocal

本文详细介绍了ThreadLocal的工作原理,如何通过弱引用避免内存泄漏,以及在实际应用中的例子,如Spring框架中的RequestContextHolder。强调了在使用ThreadLocal后,务必手动调用remove方法以防止内存泄漏。

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

使用场景
用来解决线程安全问题,如每个线程都有自己的SimpleDateFormat;还可以用来存储每个线程特有的内容,如用户信息,日志记录的traceID,省去传参的麻烦。

原理

在这里插入图片描述
如图:每个线程都有一个属性ThreadLocal.ThreadLocalMap threadLocals = null;明显它是map结构,key是ThreadlLocal本身(this),value是我们用户存储的值。每个线程可以存储多个ThreadLocal值

然后我们看ThreadLocal提供的几个方法,有get() set() remove() withInitial()方法。

 public void set(T value) {
 		//就是获取当前线程的ThreadLocalMap 
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //为空就创建,否则直接设置值
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
 }

然后看get方法

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//key即ThreadLocal对应的hashCode是个new AtomicInteger().getAndAdd(0x61c88647),算法目的均匀分配
        	//这里面还会去删除为null的entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //首次调用,就会执行我们重写的init方法设置初始值,否则返回默认null
        return setInitialValue();
    }

ThreadLocalMap是一个数组结构

static class ThreadLocalMap {
	//注意这里Entry 继承了WeakReference<ThreadLocal<?>
	 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
}

当key冲突的时候,会寻找下一个空的位置。key即ThreadLocal对应的hashCode是个new AtomicInteger().getAndAdd(0x61c88647),目的是均匀分配。

ThreadLocal的内存泄漏

内存泄漏有两种情况,key的泄漏,或者value的泄漏。

从ThreadLocalMap 的Entry extends WeakReference<ThreadLocal<?>可以看出,key即ThreadLocal继承了弱引用,如果没有强引用指向这个key,也就是只有弱引用时,下次GC就会回收掉key。

正常情况,线程执行结束后,线程t被回收 ,他里面的key和value肯定也会被回收。但是如果线程是在线程池中,线程被重复利用,线程不会死亡,那他的属性ThreadLocalMap 包括里面的value就不会被回收(如果ThreadlLcoal实例置为null,那么key是弱引用会被回收),这时就存在内存泄漏。怎么解决呢?

实际上,Java已经考虑到这个问题,会在get、resize、remove方法中检测如果key为null的entry,会把value设置为null,避免了value被强引用造成内存泄漏。但是如果我们不使用ThreadLocal,也实际上就不会使用到他的get、resize、remove方法,线程又不终止调用链就会一直存在,这样就又导致了value的泄漏。所以我们在使用完ThreadLocal后,必须手动调用remove方法,如拦截器最后finally里调用remove方法。

spring已经为我们提供了很多,如DateTimeContextHolder,RequestContextHolder,尽量使用框架提供的

应用举例

Spring框架中最明显的就是RequestContextHolder,它保存了当前请求参数,常见用法如:

ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String authorization = request.getHeader("Authorization");

它包含了两个ThreadLocal<RequestAttributes>

public abstract class RequestContextHolder  {
	private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
			new NamedThreadLocal<>("Request attributes");
	// 需要开启才能在子线程中获取父线程的值
	private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
			new NamedInheritableThreadLocal<>("Request context");
}

注意Spring框架默认是在DispatcherServlet里(父类FrameworkServlet)的processRequest()里调用了RequestContextHolder.setRequestAttributes()方法来向当前线程的ThreadLocal放入ServletRequestAttributes,这样我们就可以在当前线程里使用RequestContextHolder.getRequestAttributes()了。

除了默认的DispatcherServlet,Spring框架也会在RequestContextListener和RequestContextFilter里去设置setRequestAttributes。

### ThreadLocal 的基本概念 `ThreadLocal` 是 Java 中的一个类,用于创建线程本地变量 (thread-local variables)[^1]。这些变量的特点在于每个线程都有自己的独立副本,因此即使多个线程访问同一个 `ThreadLocal` 实例,它们也不会相互干扰。 #### 创建和初始化 ThreadLocal 变量 可以通过以下方式来定义并初始化一个 `ThreadLocal` 对象: ```java ThreadLocal<Integer> threadLocalVariable = new ThreadLocal<>(); // 或者通过提供初始值的方式 ThreadLocal<Integer> initializedThreadLocal = ThreadLocal.withInitial(() -> 0); ``` 上述代码展示了两种不同的方法来实例化 `ThreadLocal` 对象。第一种方式未指定默认值,在首次调用前会返回 `null`;第二种则提供了 lambda 表达式作为其初始值设定逻辑[^2]。 ### 使用场景分析 `ThreadLocal` 常被用来处理多线程环境下的数据隔离问题。例如数据库连接、Session 管理或者事务控制等方面都可以利用它实现每条线程拥有自己单独的状态信息而不影响其他线程的工作状态[^3]。 #### 示例程序展示如何应用 ThreadLocal 来保存当前用户的登录名 ```java public class UserContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void setUserName(String userName){ System.out.println(Thread.currentThread().getName() + ": Setting user name - "+userName); contextHolder.set(userName); } public static String getUserName(){ return contextHolder.get(); } public static void remove(){ contextHolder.remove(); // 清除资源防止内存泄漏 } } class Task implements Runnable{ private String username; public Task(String username){ this.username=username; } @Override public void run() { try { UserContextHolder.setUserName(username); // Simulate some processing time. Thread.sleep(1000L); System.out.println(Thread.currentThread().getName()+": Current UserName is "+UserContextHolder.getUserName()); } catch (InterruptedException e) { e.printStackTrace(); } finally { UserContextHolder.remove(); } } } ``` 在这个例子中,我们模拟了一个简单的任务执行过程,并且使用了 `ThreadLocal` 存储用户名以便于后续操作能够获取到正确的用户身份标识符[^4]。 ### 最佳实践建议 - **及时清理资源**:当不再需要某个特定的 `ThreadLocal` 数据时应该显式地调用 `.remove()` 方法释放关联的对象引用以防潜在的内存泄露风险。 - **避免存储大对象**:由于每个线程都会持有属于它的那份拷贝,如果存放大体积的数据结构可能会造成不必要的开销甚至 OOM 错误。 - **合理设置初值函数**:对于那些可能频繁读取却很少修改的情况可以考虑采用懒加载模式减少性能损耗[^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值