一、什么是 ThreadLocal简单来说就是一个线程局部变量。每一个访问它的线程都能独立初始化自己的变量拷贝做到了线程隔离的功能。二、为什么会出现ThreadLocal在多线程环境下会出现数据竞争的问题就是一份资源可能会被多个线程同时访问那么就很有可能造成数据混乱、数据丢失的问题。总之我们需要保障数据的安全性。这里有两种思路1. 以时间换空间Synchronized关键字就是最好的实践通过加锁只有获取到锁的线程才能访问数据数据的安全性有了保障。但是牺牲了效率。注可以回忆一下Synchronized的锁升级机制以及Synchronized与Reentrantlock的区别2. 以空间换时间ThreadLocal的做法就是让每个线程都能拥有自己需要的资源具体看底层原理。三、底层原理这里需要搞清楚ThreadLocal、Thread、ThreadLocalMap三者之间的联系。1. Thread是容器线程本身才是实际存储数据的容器每一个线程对象都有一个名为threadLocals的变量它的类型是ThreadLocal.ThreadLocalMap。2. Map的结构类似于HashMap它的key是ThreadLocal实例本身而Map才是我们存储的值。3.弱引用ThreadLocalMap的Entry中的key对ThreadLocal是弱引用。交互流程如下当调用threadLocal.set(A)时首先获取当前线程thread.currentThread()然后获取当前线程内部的threadLocals也就是Map最后把当前的ThreadLocal对象作为keyA作为Value存进去。当调用threadLocal.get()时首先也是拿到当前线程的Map然后用当前线程的ThreadLocal去找对应的Value。四、适用场景1. 最关键的一点维护全链路的上下文在复杂的后端系统中一个请求通常会经过Filter - Controller - Service - Manager - Dao。如果每一个层级都需要用到“当前登录用户的信息UserDTO”你会发现代码变得非常臃肿。如果没有ThreadLocal代码可能长这样public class OrderController { public void createOrder(UserDTO user, OrderRequest request) { // 为了传 user每个方法都要加这个参数 orderService.saveOrder(user, request); } } public class OrderService { public void saveOrder(UserDTO user, OrderRequest request) { // 校验逻辑需要 user checkInventory(user, request); // 保存逻辑也需要 user orderDao.insert(user, request); } private void checkInventory(UserDTO user, OrderRequest request) { System.out.println(Checking for user: user.getName()); } }这样写的缺点代码入侵严重即使每个中间方法都不需要user也必须声明这个参数只为了传给下游难以维护如果以后要多传一个TracertID或者TenantID就需要修复整个链路上的所有方法签名。更优雅的方案是ThreadLocal维护上下文第一步定义上下文工具类public class UserContext { // 定义一个 ThreadLocal 来存放用户信息 private static final ThreadLocalUserDTO USER_HOLDER new ThreadLocal(); public static void setUser(UserDTO user) { USER_HOLDER.set(user); } public static UserDTO getUser() { return USER_HOLDER.get(); } public static void remove() { USER_HOLDER.remove(); // 记得清理防止内存泄漏 } }第二步在入口处拦截器或过滤器存入数据这是最关键的一步。在请求进来时我们就把用户信息塞进这个线程。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, ...) { // 1. 从 Session 或 Token 中获取用户信息 UserDTO user userService.getUserFromToken(request.getHeader(Authorization)); // 2. 存入 ThreadLocal UserContext.setUser(user); return true; } Override public void afterCompletion(...) { // 3. 请求结束一定要清理掉否则线程回到线程池后数据还在 UserContext.remove(); } }第三步在业务层按需取用无论在哪一层只要是同一个线程在执行随时随地都能拿到用户。public class OrderService { public void saveOrder(OrderRequest request) { // 无需通过参数传递直接从“空气”中抓取当前用户 UserDTO currentUser UserContext.getUser(); System.out.println(当前下单用户是: currentUser.getName()); // 调用复杂嵌套的方法也不需要传 user 了 doSomeDeepLogic(); } private void doSomeDeepLogic() { // 深层方法依然能拿到 UserDTO user UserContext.getUser(); // ... 业务逻辑 } }五、关于内存泄漏ThreadLocal也有缺点那就是它容易内存泄漏。这里的伏笔在于ThreadLocalMap的key是弱引用但是Value是强引用。如果ThreadLocal变量没有被外部强引用了key就会被回收变成null。如果没有手动remove那么这个Entry里的Value就会一直存在线程的Map里直到线程结束。但是这在线程池里非常危险因为线程池里的线程是复用的如果上一个任务没有remove下一个任务可能就直接拿到旧数据也不会被回收。所以用完必须remove。try { threadLocal.set(value); // 业务逻辑 } finally { threadLocal.remove(); // 必须在 finally 里清理 }
ThreadLocal详解
一、什么是 ThreadLocal简单来说就是一个线程局部变量。每一个访问它的线程都能独立初始化自己的变量拷贝做到了线程隔离的功能。二、为什么会出现ThreadLocal在多线程环境下会出现数据竞争的问题就是一份资源可能会被多个线程同时访问那么就很有可能造成数据混乱、数据丢失的问题。总之我们需要保障数据的安全性。这里有两种思路1. 以时间换空间Synchronized关键字就是最好的实践通过加锁只有获取到锁的线程才能访问数据数据的安全性有了保障。但是牺牲了效率。注可以回忆一下Synchronized的锁升级机制以及Synchronized与Reentrantlock的区别2. 以空间换时间ThreadLocal的做法就是让每个线程都能拥有自己需要的资源具体看底层原理。三、底层原理这里需要搞清楚ThreadLocal、Thread、ThreadLocalMap三者之间的联系。1. Thread是容器线程本身才是实际存储数据的容器每一个线程对象都有一个名为threadLocals的变量它的类型是ThreadLocal.ThreadLocalMap。2. Map的结构类似于HashMap它的key是ThreadLocal实例本身而Map才是我们存储的值。3.弱引用ThreadLocalMap的Entry中的key对ThreadLocal是弱引用。交互流程如下当调用threadLocal.set(A)时首先获取当前线程thread.currentThread()然后获取当前线程内部的threadLocals也就是Map最后把当前的ThreadLocal对象作为keyA作为Value存进去。当调用threadLocal.get()时首先也是拿到当前线程的Map然后用当前线程的ThreadLocal去找对应的Value。四、适用场景1. 最关键的一点维护全链路的上下文在复杂的后端系统中一个请求通常会经过Filter - Controller - Service - Manager - Dao。如果每一个层级都需要用到“当前登录用户的信息UserDTO”你会发现代码变得非常臃肿。如果没有ThreadLocal代码可能长这样public class OrderController { public void createOrder(UserDTO user, OrderRequest request) { // 为了传 user每个方法都要加这个参数 orderService.saveOrder(user, request); } } public class OrderService { public void saveOrder(UserDTO user, OrderRequest request) { // 校验逻辑需要 user checkInventory(user, request); // 保存逻辑也需要 user orderDao.insert(user, request); } private void checkInventory(UserDTO user, OrderRequest request) { System.out.println(Checking for user: user.getName()); } }这样写的缺点代码入侵严重即使每个中间方法都不需要user也必须声明这个参数只为了传给下游难以维护如果以后要多传一个TracertID或者TenantID就需要修复整个链路上的所有方法签名。更优雅的方案是ThreadLocal维护上下文第一步定义上下文工具类public class UserContext { // 定义一个 ThreadLocal 来存放用户信息 private static final ThreadLocalUserDTO USER_HOLDER new ThreadLocal(); public static void setUser(UserDTO user) { USER_HOLDER.set(user); } public static UserDTO getUser() { return USER_HOLDER.get(); } public static void remove() { USER_HOLDER.remove(); // 记得清理防止内存泄漏 } }第二步在入口处拦截器或过滤器存入数据这是最关键的一步。在请求进来时我们就把用户信息塞进这个线程。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, ...) { // 1. 从 Session 或 Token 中获取用户信息 UserDTO user userService.getUserFromToken(request.getHeader(Authorization)); // 2. 存入 ThreadLocal UserContext.setUser(user); return true; } Override public void afterCompletion(...) { // 3. 请求结束一定要清理掉否则线程回到线程池后数据还在 UserContext.remove(); } }第三步在业务层按需取用无论在哪一层只要是同一个线程在执行随时随地都能拿到用户。public class OrderService { public void saveOrder(OrderRequest request) { // 无需通过参数传递直接从“空气”中抓取当前用户 UserDTO currentUser UserContext.getUser(); System.out.println(当前下单用户是: currentUser.getName()); // 调用复杂嵌套的方法也不需要传 user 了 doSomeDeepLogic(); } private void doSomeDeepLogic() { // 深层方法依然能拿到 UserDTO user UserContext.getUser(); // ... 业务逻辑 } }五、关于内存泄漏ThreadLocal也有缺点那就是它容易内存泄漏。这里的伏笔在于ThreadLocalMap的key是弱引用但是Value是强引用。如果ThreadLocal变量没有被外部强引用了key就会被回收变成null。如果没有手动remove那么这个Entry里的Value就会一直存在线程的Map里直到线程结束。但是这在线程池里非常危险因为线程池里的线程是复用的如果上一个任务没有remove下一个任务可能就直接拿到旧数据也不会被回收。所以用完必须remove。try { threadLocal.set(value); // 业务逻辑 } finally { threadLocal.remove(); // 必须在 finally 里清理 }