当前位置: 代码网 > it编程>编程语言>Java > Java项目开发中ThreadLocal的6大用法总结

Java项目开发中ThreadLocal的6大用法总结

2026年01月26日 Java 我要评论
前言你的系统需要记录每个用户的操作日志,包括用户id、操作时间、操作内容等。在单线程环境下,这很简单,一个全局变量就够了。但到了web应用中,一个请求一个线程,多个用户同时操作,怎么保证a用户的操作不

前言

你的系统需要记录每个用户的操作日志,包括用户id、操作时间、操作内容等。在单线程环境下,这很简单,一个全局变量就够了。

但到了web应用中,一个请求一个线程,多个用户同时操作,怎么保证a用户的操作不会被记录成b用户呢?

你可能会想到在每个方法里都传递用户信息,但这样太麻烦了。

这就是 threadlocal 要解决的核心问题:在多线程环境下,如何在不传递参数的情况下,让同一个线程内的多个方法共享数据,同时又不会影响其他线程?

threadlocal是什么

简单理解,threadlocal 是一个让每个线程拥有自己独立变量副本的容器。

对同一个 threadlocal 实例的 get()/set(),不同线程看到的是不同的数据,互不干扰。

看个最简单的例子:

public class usercontext {
    // 创建一个threadlocal变量,用来存储用户id
    private static final threadlocal<string> useridholder = new threadlocal<>();
    
    // 设置用户id
    public static void setuserid(string userid) {
        useridholder.set(userid);
    }
    
    // 获取用户id
    public static string getuserid() {
        return useridholder.get();
    }
    
    // 清理用户id
    public static void clear() {
        useridholder.remove();
    }
}

使用起来是这样的:

try {
    usercontext.setuserid(userid);
    // 业务处理...
} finally {
    // 确保处理完请求后,无论如何都会清理
    usercontext.clear(); 
}

// 在后续的业务逻辑中,任何地方都可以获取到这个用户id
string userid = usercontext.getuserid();

是不是很方便?不用在每个方法参数里传递用户id,任何地方都可以获取到当前线程的用户信息。

threadlocal是怎么工作的

很多人以为 threadlocal 只是简单的把变量复制到每个线程,其实不是这样。

它的原理是这样的:

  • 每个线程(thread对象)内部都有一个特殊的map,叫做 threadlocalmap
  • 当你调用 threadlocal.set(value) 时,实际上是把 value 放到了当前线程的 threadlocalmap 中,key 是这个 threadlocal 对象。
  • 当你调用 threadlocal.get() 时,实际上是去当前线程的 threadlocalmap 中,找这个 threadlocal 对象对应的 value。

可以想象成每个线程都有一个小本本(threadlocalmap),这个小本本上记录着各种 threadlocal 变量的值。

当你调用某个 threadlocal 的 get 方法,就是在小本本上查找对应的内容。

threadlocal 使用场景和示例

1. 用户会话管理(最常用)

在web开发中,这是最经典的使用场景。用户登录后,将用户信息存入 threadlocal,在后续的任何地方都能获取。

// 用户上下文工具类
public class usercontext {
    private static final threadlocal<userinfo> currentuser = new threadlocal<>();
    
    public static void setuser(userinfo user) {
        currentuser.set(user);
    }
    
    public static userinfo getuser() {
        return currentuser.get();
    }
    
    public static long getuserid() {
        userinfo user = currentuser.get();
        return user != null ? user.getid() : null;
    }
    
    public static string getusername() {
        userinfo user = currentuser.get();
        return user != null ? user.getusername() : null;
    }
    
    public static void clear() {
        currentuser.remove();
    }
}

// 在拦截器中设置用户信息
@component
public class authinterceptor implements handlerinterceptor {
    @override
    public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) {
        // 从token或session中获取用户信息
        string token = request.getheader("authorization");
        userinfo user = authservice.getuserbytoken(token);
        
        if (user != null) {
            usercontext.setuser(user);
        }
        return true;
    }
    
    @override
    public void aftercompletion(httpservletrequest request, httpservletresponse response, object handler, exception ex) {
        // 请求结束后清理threadlocal,防止内存泄漏
        usercontext.clear();
    }
}

// 在业务代码中直接使用
@service
public class orderservice {
    public void createorder(order order) {
        // 直接获取当前用户id,不需要从参数传递
        long userid = usercontext.getuserid();
        order.setuserid(userid);
        order.setcreateby(usercontext.getusername());
        
        // 其他业务逻辑...
        orderdao.save(order);
        
        // 记录操作日志
        logservice.log(userid, "创建订单");
    }
}

2. 日期格式化工具

simpledateformat 是线程不安全的,用 threadlocal 可以解决这个问题。

// 错误示例:多线程下会出问题
public class dateutils {
    private static final simpledateformat sdf = new simpledateformat("yyyy-mm-dd hh:mm:ss");
    
    public static string format(date date) {
        return sdf.format(date);  // 多线程并发时会出错!
    }
}

// 正确示例:使用threadlocal
public class threadsafedateutils {
    // 每个线程都有自己的simpledateformat实例
    private static final threadlocal<simpledateformat> threadlocal = 
        threadlocal.withinitial(() -> new simpledateformat("yyyy-mm-dd hh:mm:ss"));
    
    public static string format(date date) {
        return threadlocal.get().format(date);
    }
    
    public static date parse(string datestr) throws parseexception {
        return threadlocal.get().parse(datestr);
    }
    
    // 使用后清理(可选,因为dateformat对象可以复用)
    public static void clear() {
        threadlocal.remove();
    }
}

3. 分页信息管理

在分页查询中,threadlocal可以保存分页参数,在service和dao层都能获取。

public class pagecontext {
    
    private static final threadlocal<integer> page_no = new threadlocal<>();
    private static final threadlocal<integer> page_size = new threadlocal<>();
    
    public static void setpageinfo(integer pageno, integer pagesize) {
        page_no.set(pageno);
        page_size.set(pagesize);
    }
    
    public static integer getpageno() {
        integer pageno = page_no.get();
        return pageno != null ? pageno : 1; // 默认第一页
    }
    
    public static integer getpagesize() {
        integer pagesize = page_size.get();
        return pagesize != null ? pagesize : 20; // 默认20条
    }
    
    public static void clear() {
        page_no.remove();
        page_size.remove();
    }
}

// 在controller中使用
@getmapping("/users")
public pageresult<user> getusers(@requestparam(defaultvalue = "1") integer pageno,
                                 @requestparam(defaultvalue = "20") integer pagesize) {
    try {
        pagecontext.setpageinfo(pageno, pagesize);
        return userservice.getuserlist();
    } finally {
        pagecontext.clear();
    }
}

4. 数据库连接管理

在一些框架中,为了确保同一个事务中使用同一个数据库连接,会用到 threadlocal

public class connectionholder {
    private static final threadlocal<connection> connectionholder = new threadlocal<>();
    
    public static connection getconnection() {
        connection conn = connectionholder.get();
        if (conn == null) {
            conn = datasourceutils.getconnection();
            connectionholder.set(conn);
        }
        return conn;
    }
    
    public static void setconnection(connection conn) {
        connectionholder.set(conn);
    }
    
    public static void clearconnection() {
        connection conn = connectionholder.get();
        if (conn != null) {
            datasourceutils.releaseconnection(conn);
        }
        connectionholder.remove();
    }
}

5. 全局参数传递

避免在方法调用链中层层传递相同参数,简化代码。

// 不用threadlocal,参数传递很痛苦
public void processorder(string traceid, string userid, order order) {
    validateorder(traceid, userid, order);
    checkinventory(traceid, userid, order);
    createlog(traceid, userid, order);
    // ... 更多调用
}

// 使用threadlocal,清爽多了
public void processorder(order order) {
    // 在入口处设置
    tracecontext.settraceid(uuid.randomuuid().tostring());
    usercontext.setuserid(getcurrentuserid());
    
    validateorder(order);
    checkinventory(order);
    createlog(order);
}

6. 分布式追踪id传递

在微服务架构中,一个请求可能会经过多个服务,需要有一个traceid来追踪整个调用链。

@component
public class tracefilter implements filter {
    
    @override
    public void dofilter(servletrequest request, servletresponse response, 
                         filterchain chain) throws ioexception, servletexception {
        
        httpservletrequest httprequest = (httpservletrequest) request;
        string traceid = httprequest.getheader("x-trace-id");
        
        if (traceid == null || traceid.isempty()) {
            traceid = uuid.randomuuid().tostring();
        }
        
        // 设置到threadlocal
        tracecontext.settraceid(traceid);
        
        try {
            // 在mdc中也设置,方便日志打印
            mdc.put("traceid", traceid);
            
            // 继续处理请求
            chain.dofilter(request, response);
        } finally {
            // 一定要清理!!!
            tracecontext.clear();
            mdc.clear();
        }
    }
}

threadlocal 可能出现的问题

1. 内存泄漏(最重要的问题!)

这是threadlocal最容易被忽视,也是最危险的问题。我们先来看看为什么会内存泄漏。

public class memoryleakexample {
    
    private static final threadlocal<bigobject> holder = new threadlocal<>();
    
    public void process() {
        // 设置一个大对象
        holder.set(new bigobject()); // 假设bigobject占用100mb内存
        
        // 执行业务逻辑...
        // ...
        
        // 忘记调用 holder.remove() 了!
    }
}

问题

  • 当方法执行完后,threadlocal 对象本身(holder)可能被回收
  • threadlocalmap 中,bigobject 这个 100mb 的对象仍然被引用着
  • 如果这个线程是线程池中的线程,会被复用,永远不会被gc回收
  • 随着请求增多,内存占用会越来越大,最终导致oom

为什么会这样? 看一下 threadlocalmap 的内部结构:

static class threadlocalmap {
    // entry继承了weakreference,key是弱引用
    static class entry extends weakreference<threadlocal<?>> {
        object value; // value是强引用!
    }
    
    private entry[] table;
}

内存泄漏的两种情况:

1.threadlocal 对象被回收,但value还在

  • key是弱引用,threadlocal 对象被回收后,key变成null
  • 但value是强引用,还在 entry 中被引用着
  • 如果线程不结束,这个value就永远无法被回收

2.线程结束,但 threadlocalmap 还在

  • thread 对象有 threadlocalmap 的引用
  • thread 结束,thread 对象可以被回收
  • 但如果 threadlocal 对象还被其他地方引用,就可能导致 threadlocalmap 无法被完全回收

2. 线程池中的值污染

在线程池环境下,线程会被复用。如果上一个任务设置了threadlocal值但没有清理,下一个任务可能会读到错误的值。

// 线程池
executorservice executor = executors.newfixedthreadpool(5);

// 任务1:用户a的操作
executor.execute(() -> {
    usercontext.setuserid("usera");
    // 执行业务逻辑...
    // 忘记清理了!
});

// 任务2:用户b的操作
executor.execute(() -> {
    // 这里可能获取到usera的id!
    string userid = usercontext.getuserid();
    system.out.println("当前用户:" + userid); // 输出:usera
});

3. 父子线程值传递问题

默认情况下,子线程无法获取父线程的 threadlocal 值。

public class parentchildexample {
    private static final threadlocal<string> holder = new threadlocal<>();
    
    public static void main(string[] args) {
        holder.set("父线程的值");
        
        new thread(() -> {
            // 子线程获取不到父线程的值
            system.out.println("子线程:" + holder.get()); // 输出:null
        }).start();
    }
}

threadlocal 问题的解决方案

方案1:一定要记得清理(最重要!)

黄金法则:每次使用完threadlocal,一定要调用remove()方法。

public class safeusercontext {
    private static final threadlocal<user> context = new threadlocal<>();
    
    public static void set(user user) {
        context.set(user);
    }
    
    public static user get() {
        return context.get();
    }
    
    public static void clear() {
        context.remove(); // 清理!
    }
}

// 使用try-finally确保一定会清理
public void processrequest(httpservletrequest request) {
    try {
        user user = parseuser(request);
        safeusercontext.set(user);
        
        // 执行业务逻辑
        dobusiness();
        
    } finally {
        // 无论是否发生异常,都会执行清理
        safeusercontext.clear();
    }
}

方案2:使用拦截器/过滤器统一管理

在web应用中,可以使用拦截器或过滤器统一管理threadlocal的生命周期。

@component
public class threadlocalcleaninterceptor implements handlerinterceptor {
    
    @override
    public boolean prehandle(httpservletrequest request, 
                            httpservletresponse response, object handler) {
        // 在请求开始时设置threadlocal
        string traceid = request.getheader("x-trace-id");
        tracecontext.settraceid(traceid);
        
        return true;
    }
    
    @override
    public void aftercompletion(httpservletrequest request, 
                               httpservletresponse response, 
                               object handler, exception ex) {
        // 请求结束后清理所有threadlocal
        tracecontext.clear();
        usercontext.clear();
        pagecontext.clear();
        // ... 清理其他threadlocal
    }
}

方案3:使用inheritablethreadlocal传递值

如果需要父子线程间传递值,可以使用inheritablethreadlocal

public class inheritablecontext {
    private static final inheritablethreadlocal<string> context = 
        new inheritablethreadlocal<>();
    
    public static void main(string[] args) {
        context.set("父线程的值");
        
        new thread(() -> {
            // 子线程可以获取到父线程的值
            system.out.println("子线程:" + context.get()); // 输出:父线程的值
            
            // 子线程修改值,不会影响父线程
            context.set("子线程修改后的值");
        }).start();
        
        thread.sleep(100);
        system.out.println("父线程:" + context.get()); // 输出:父线程的值
    }
}

注意:线程池场景下慎用,因为线程是复用的,不是新创建的。

方案4:使用阿里开源的 transmittablethreadlocal

对于线程池场景,可以考虑使用阿里的transmittablethreadlocal,它是inheritablethreadlocal的增强版。

<!-- 添加依赖 -->
<dependency>
    <groupid>com.alibaba</groupid>
    <artifactid>transmittable-thread-local</artifactid>
    <version>2.14.2</version>
</dependency>
public class transmittableexample {
    
    private static final transmittablethreadlocal<string> context = 
        new transmittablethreadlocal<>();
    
    public static void main(string[] args) {
        executorservice executor = executors.newfixedthreadpool(2);
        
        // 使用ttlexecutors包装线程池
        executorservice ttlexecutor = ttlexecutors.getttlexecutorservice(executor);
        
        context.set("value-1");
        
        ttlexecutor.execute(() -> {
            system.out.println("任务1:" + context.get()); // 输出:value-1
        });
        
        context.set("value-2");
        
        ttlexecutor.execute(() -> {
            system.out.println("任务2:" + context.get()); // 输出:value-2
        });
    }
}

方案5:threadlocal的封装

我们可以封装一个安全的threadlocal工具类。

public class safethreadlocal<t> {
    
    private final threadlocal<t> threadlocal = new threadlocal<>();
    
    // 设置值,并返回一个cleanup对象
    public cleanup set(t value) {
        threadlocal.set(value);
        return new cleanup();
    }
    
    public t get() {
        return threadlocal.get();
    }
    
    public void remove() {
        threadlocal.remove();
    }
    
    // cleanup类,用于自动清理
    public class cleanup implements autocloseable {
        @override
        public void close() {
            threadlocal.remove();
        }
    }
}

// 使用示例:try-with-resources自动清理
public void process() {
    safethreadlocal<string> safeholder = new safethreadlocal<>();
    
    try (safethreadlocal<string>.cleanup cleanup = safeholder.set("value")) {
        // 执行业务逻辑
        string value = safeholder.get();
        // ...
    } // 这里会自动调用cleanup.close(),清理threadlocal
}

写在最后

threadlocal 是java并发编程中的一个重要工具,它解决了多线程环境下的数据隔离问题。但任何强大的工具一样,它都需要被正确的使用。

该用threadlocal的时候

  • 需要在线程内共享数据,但不想用方法参数传递
  • 需要保证线程安全的数据隔离
  • 工具类需要线程安全但不想用同步锁

看到这,相信你对 threadlocal 已经不再是那么陌生了。

以上就是java项目开发中threadlocal的6大用法总结的详细内容,更多关于java threadlocal用法的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com