ThreadLocal笔记
使用建议
《Effective Java》的建议:避免滥用ThreadLocal,使用remove()防止内存泄漏,使用withInitial方法提供初始值。
threadlocal的set/get操作,是如何与Thread关联的
线程中有一个属性叫threadLocals,它是一个map,也就是说线程可以关联多个tl。
tl设置的值,会以 tl,value 键值的形式放入当前线程的threadLocalMap中,从而完成绑定
public class ThreadLocal<T> {
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocals
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 创建新的,在赋值给线程
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
// this就是ThreadLocal对象,作为弱引用传入
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
static class ThreadLocalMap {
// 条目数据都是弱引用类型
static class Entry extends WeakReference<ThreadLocal<?>> {}
}
ThreadLocalMap中的Entry继承WeakReference对象,entry的构造方法中使用super(k),k 就是ThreadLocal对象.所以线程ThreadLocalMap中的key都是弱引用.
Entry之所以使用弱引用,是为了防止内存泄露.当ThreadLocal不再使用时,就会被GC回收,
即使ThreadLocalMap中的key在引用,因为是弱引用,所有仍会被GC回收.map中key为null时导致value永远无法被访问,如果不删除value,依旧存在内存泄露问题.
正确使用方式:
- 在使用完ThreadLocal时要记得remove,将thread对象tlMap中的<tl, value>移除;
- 使用private static final 修饰tl,保证tl变量的封装性和全局唯一性。
- 所有线程共享同一个 ThreadLocal 实例,线程以该tl实例作为key,将具体的值放入thread自己的ThreadLocalMap中,线程间互不影响
// todo 拓展: 强软弱虚引用
线程池场景下的问题
- 数据污染问题
tl是线程的本地副本,而线程池又存在线程的复用,如果线程执行一次任务后没有调用tl.remove,则线程在下次复用时,tl中的值还在。 - 父子线程数据传递
InheritableThreadLocal可以将父线程数据传递给子线程,但子线程无法获取后续tl更新的值 - TransmittableThreadLocal可以解决父进程向子进程传递数据的问题
public static void ttlInThreadPoll() throws InterruptedException {
String mainName = Thread.currentThread().getName();
TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();
ttl.set("haha");
System.out.println(mainName + "获取:"+ttl.get());
ExecutorService executor = Executors.newSingleThreadExecutor();
executor = TtlExecutors.getTtlExecutorService(executor);// 需要包装线程池
try{
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "获取:"+ ttl.get());
});
Thread.sleep(1000);
ttl.set("haha2");
System.out.println(mainName + "获取新值:"+ttl.get());
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "获取新值:"+ ttl.get());
});
}finally {
executor.shutdown();
}
}
ThreadLocal使用场景
Request Context(请求上下文)
在处理HTTP请求时,ThreadLocal可以用来存储与当前请求相关的信息,例如用户身份、会话信息等。这可以在请求处理的各个层(Controller、Service等)中访问,而不需要显式地传递参数。
public class RequestContext {
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
public static void setUser(User user) {
userHolder.set(user);
}
public static User getUser() {
return userHolder.get();
}
public static void clear() {
userHolder.remove();
}
}
// 在某个Filter中设置用户信息
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
User user = authenticate((HttpServletRequest) request);
RequestContext.setUser(user);
try {
chain.doFilter(request, response);
} finally {
RequestContext.clear();
}
}
}
事务管理:
Spring的事务管理器使用ThreadLocal来存储事务上下文,以确保在同一个线程中执行的多个数据库操作能够共享同一个事务上下文。
public class TransactionManager {
private static final ThreadLocal<Transaction> currentTransaction = new ThreadLocal<>();
public static void beginTransaction() {
Transaction tx = new Transaction();
currentTransaction.set(tx);
}
public static Transaction getCurrentTransaction() {
return currentTransaction.get();
}
public static void commitTransaction() {
Transaction tx = currentTransaction.get();
if (tx != null) {
tx.commit();
currentTransaction.remove();
}
}
public static void rollbackTransaction() {
Transaction tx = currentTransaction.get();
if (tx != null) {
tx.rollback();
currentTransaction.remove();
}
}
}
日志跟踪:
在分布式系统中,可以使用ThreadLocal存储唯一的请求ID,以便在日志中进行请求跟踪。
比如使用ThreadLocal进行traceId的暂存,在每个请求进入服务时,通过过滤器或拦截器进行traceId的统一获取
// 过滤器
@Slf4j
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("过滤器执行");
String traceId = request.getHeader("Trace-ID");
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString();
}
TraceIdHolder.setTraceId(traceId);
log.info("traceId = {}", traceId);
try {
filterChain.doFilter(request, response);
} finally {
TraceIdHolder.clear();
}
}
}
//ThreadLocal
@Slf4j
public class TraceIdHolder {
private static final ThreadLocal<String> traceIdHolder = ThreadLocal.withInitial(() -> null);
public static void setTraceId(String traceId) {
traceIdHolder.set(traceId);
}
public static String getTraceId() {
return traceIdHolder.get();
}
public static void clear() {
traceIdHolder.remove();
}
public static HttpEntity<Map<String, String>> buildHttpEntity(){
String traceId = TraceIdHolder.getTraceId();
if(traceId == null) {
throw new RuntimeException("当前traceId不存在");
}
HttpHeaders headers = new HttpHeaders();
headers.set("Trace-ID", traceId);
return new HttpEntity<>(headers);
}
}
// 调用
@RequestMapping("/serviceA")
@ResponseBody
public String serviceA() {
System.out.println("serviceA");
HttpEntity<Map<String, String>> entity = TraceIdHolder.buildHttpEntity();
// RPC通信框架,追踪代码如何不侵入业务?
// dubbo提供RpcContext可以用来传递参数
ResponseEntity<String> response = restTemplate.exchange("http://localhost:9002/serviceB", HttpMethod.GET, entity, String.class);
return "Hello serviceA";
}