前言
你的系统需要记录每个用户的操作日志,包括用户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用法的资料请关注代码网其它相关文章!
发表评论