ThreadLocal
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
适用场景
一个线程在一个类中需要执行不同的方法,同时这些方法都需要相同的入参,就可以定义一个上下文环境
📌只是演示使用ThreadLocal,实际上ThreadLocal不支持父子线程的上下文传递,以及多线程间的上下文传递,应使用它的子类
InheritableThreadLocal便可解决父子线程的上下文传递这个问题,但是依旧不可多线程间上下文传递,应使用阿里的TransmittableThreadLocal这个类,其继承InheritableThreadLocal,借助该jar包对线程池的再一层封装,便可实现多线程间的上下文传递。
创建一个上下文
package com.strap.mydemo.thread;
/**
* <p>定义一个上下文</p>
*
* @author strap
* @since 2023/3/20 19:50
*/
public class UserContext {
/**
* 用户名
*/
private final String userName;
/**
* 登录token
*/
private final String token;
/**
* 过期时间
*/
private final Long exp;
private UserContext(String userName, String token, Long exp) {
this.userName = userName;
this.token = token;
this.exp = exp;
}
public String getUserName() {
return userName;
}
public String getToken() {
return token;
}
public Long getExp() {
return exp;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String userName;
private String token;
private Long exp;
public Builder userName(String userName) {
this.userName = userName;
return this;
}
public Builder token(String token) {
this.token = token;
return this;
}
public Builder exp(Long exp) {
this.exp = exp;
return this;
}
public UserContext build() {
return new UserContext(this.userName, this.token, this.exp);
}
}
}
定义ThreadLocal存储上下文
package com.strap.mydemo.thread;
import cn.hutool.core.lang.Opt;
import java.util.function.Function;
/**
* <p>自定义上下文环境</p>
*
* @author strap
* @since 2023/3/20 19:51
*/
public class UserContextHolder {
private static final ThreadLocal<UserContext> THREAD_USER_CONTEXT = new ThreadLocal<>();
public static UserContext getContext() {
return THREAD_USER_CONTEXT.get();
}
public static void initContext(UserContext businessContext) {
THREAD_USER_CONTEXT.set(businessContext);
}
public static <T> T field(Function<UserContext, T> getter) {
return Opt.ofNullable(THREAD_USER_CONTEXT.get()).map(getter).get();
}
public static void clearContext() {
THREAD_USER_CONTEXT.remove();
}
}
测试
package com.strap.mydemo.interceptors;
import com.strap.mydemo.service.IUserDetailService;
import com.strap.mydemo.thread.UserContextHolder;
import lombok.extern.log4j.Log4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p></p>
*
* @author strap
* @since 2023/3/15 14:03
*/
@Log4j
public class LoginInterceptor implements HandlerInterceptor {
private final IUserDetailService userDetailService;
public LoginInterceptor(IUserDetailService userDetailService) {
this.userDetailService = userDetailService;
}
@Override
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, Object handler) throws Exception {
return userDetailService.verify(request);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 每次处理完成,记得清空上下文环境
UserContextHolder.clearContext();
}
}
vertify方法
@Override
public boolean verify(HttpServletRequest request) throws Exception {
String token = StrUtil.removeAll(request.getHeader(Header.AUTHORIZATION.getValue()), TOKEN_HEADER_PREFIX);
BaseResultType.ILLEGAL_ARG.isEmpty(token, () -> "token cannot be empty");
JWT jwt = JWT.of(token);
Long exp = Convert.toLong(jwt.getPayload(MyDemoConstants.UserConstants.JWT_EXPIRE_KEY), 0L);
BaseResultType.TOKEN_EXPIRE.isError(exp < System.currentTimeMillis(), () -> "token has expired");
boolean verify = jwt.verify(JWTSignerUtil.hs256(getLoginSecretStr()));
if (verify) {
// 登录验证成功,初始化上下文
UserContextHolder.initContext(
UserContext.builder()
.token(token)
.userName(String.valueOf(jwt.getPayload("userName")))
.exp(exp)
.build()
);
}
BaseResultType.FAILED.isError(!verify, () -> "invalid token");
return verify;
}
controller中不同方法获取上下文
@GetMapping(path = "/queryData")
public String quueryData() throws Exception {
methodA();
methodB();
return "查询数据";
}
public void methodA(){
log.info(UserContextHolder.field(UserContext::getUserName));
}
public void methodB(){
log.info(UserContextHolder.field(UserContext::getToken));
}
InheritableThreadLocal
解决父子线程的上下文传递问题
private static final InheritableThreadLocal<UserContext> THREAD_USER_CONTEXT = new InheritableThreadLocal<>();
TransmittableThreadLocal
解决引用线程池时的上下文传递问题
引入对应坐标依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
替换上下文中使用的ThreadLocal
private static final TransmittableThreadLocal<UserContext> THREAD_USER_CONTEXT = new TransmittableThreadLocal<>();
对线程池封装
关键是要适用阿里的线程池工具对线程池进行封装TtlExecutors.getTtlExecutor(executor),才可以在线程池中取线程时也能实现上下文信息的传递
package com.strap.mydemo.config;
import com.alibaba.ttl.threadpool.TtlExecutors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* <p></p>
*
* @author strap
* @since 2023/3/21 10:48
*/
@EnableAsync
@Configuration
public class ThreadConfig {
public ThreadConfig() {
}
@Bean("demo-async-executor")
public Executor AsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数,设置成1方便观察
executor.setCorePoolSize(1);
// 线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程,设置成1方便观察
executor.setMaxPoolSize(1);
// 缓存队列
executor.setQueueCapacity(20);
// 空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(200);
// 异步方法内部线程名称
executor.setThreadNamePrefix("demo-async-executor");
// 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
// 通常有以下四种策略:
// ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
// ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
// ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
// ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return TtlExecutors.getTtlExecutor(executor);
}
}
测试
@GetMapping(path = "/queryData")
public String quueryData() throws Exception {
methodA();
iUserDetailService.methodC();
methodB();
return "查询数据";
}
public void methodA(){
log.info(Thread.currentThread().getName() + "--" + UserContextHolder.field(UserContext::getUserName));
}
public void methodB(){
log.info(Thread.currentThread().getName() + "--" + UserContextHolder.field(UserContext::getToken));
}
接口实现类增加异步线程方法
@Override
@Async("demo-async-executor")
public void methodC() throws Exception{
log.info(Thread.currentThread().getName() + "--" + UserContextHolder.field(UserContext::getToken));
}
结果
[com.strap.mydemo.controller.TestController]-https-jsse-nio-8443-exec-1–test
[com.strap.mydemo.controller.TestController]-https-jsse-nio-8443-exec-1–aa
[com.strap.mydemo.service.impl.UserDetailServiceImpl]-demo-async-executor1–aa

10万+





