ThreadLocal详解与实战指南
目录
1. ThreadLocal基本概念
1.1 核心原理
1.2 主要特性
2. ThreadLocal API详解
2.1 核心方法
2.2 基本使用方式
3. ThreadLocal使用场景与实战
3.1 场景一:用户身份信息传递
实现步骤
1.创建用户上下文类
2.创建过滤器或拦截器来设置和清理用户信息
3.业务逻辑中使用
4.总结
3.2 场景二:事务管理
实现步骤
1.创建数据库连接管理类
2.创建事务管理工具类
3.在业务代码中使用
4.总结
3.3 场景三:简化日期格式化
实现步骤
1.创建日期工具类
2. 在多线程环境中使用
3.4 场景四:追踪请求链路
实现步骤
1.创建追踪上下文类
2.创建请求拦截器
3.配置拦截器
4.创建日志工具类(可自定义)
5.创建RestTemplate拦截器传递追踪ID
6.配置RestTemplate
7. 在业务代码中使用
3.5 场景五:slf4j——MDC全局日志打印
实现步骤
1.配置lombok(日志约束xml文件)
2.创建拦截器
3.配置拦截器
4.呈现效果
3.6 场景六:线程安全的数据缓存
实现步骤
1.创建本地缓存类
2.使用例子
4. ThreadLocal实现继承性 - InheritableThreadLocal
4.1 基本用法
4.2 用于跨线程上下文传递
实现步骤
1.创建上下文类
2.创建异步执行器
3.在多线程环境中使用
4.Controller引入
5. ThreadLocal常见问题与最佳实践
5.1 内存泄漏问题
最佳实践
1.总是调用remove方法
2. 使用try-with-resources模式
5.2 父子线程值传递问题
使用TransmittableThreadLocal
1.依赖配置
2.基本实践
3.Spring集成
5.3 ThreadLocal的值修改
最佳实践
5.4 初始化ThreadLocal值
6. 总结
1. ThreadLocal基本概念
ThreadLocal是Java提供的一个线程本地变量工具,它允许我们创建只能被同一个线程读写的变量。从线程的角度看,就好像是线程在访问自己的私有变量一样,避免了共享变量可能带来的并发问题。
在这里强调一点,ThreadLocal是绝对不会应用于线程同步的场景的!
ThreadLocal本质:隔离线程数据!而非共享数据!
这一点本文会有意无意多次强调!所以大家在学习TheadLocal的时候,要摒弃掉JUC那一套理论!大伙儿只要清楚,ThreadLocal就是多线程对于共享变量的独立备份,至于这个共享变量是否同时只能被一个线程修改,这一点是无关紧要的。
因为每个线程栈的上下文都是独立、私有的,所以Threadlocal修饰的变量存储的值相对来说肯定也是独立、私有的。
1.1 核心原理
ThreadLocal的工作原理可以简单概括为:
- 每个Thread对象内部维护了一个ThreadLocalMap
- ThreadLocalMap是一个定制化的哈希表,其中key是ThreadLocal对象的弱引用,value是该线程存储的变量副本
- 当线程访问ThreadLocal变量时,实际是在访问自己的ThreadLocalMap中对应的条目
1.2 主要特性
- 线程隔离性:每个线程拥有各自独立的变量副本
- 减少同步:避免多线程环境下的同步操作
- 方便传递上下文:无需通过参数传递上下文信息
- 资源清理:需要手动移除不再使用的ThreadLocal变量
2. ThreadLocal API详解
2.1 核心方法
方法 | 描述 |
---|---|
T get() | 获取当前线程的ThreadLocal变量副本 |
void set(T value) | 设置当前线程的ThreadLocal变量副本 |
void remove() | 移除当前线程的ThreadLocal变量副本 |
protected T initialValue() | 返回ThreadLocal变量的初始值(默认返回null) |
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) | Java 8引入,创建带初始值的ThreadLocal |
这里列出了5个,但是只要记住前三个就行:获取值、设置值、移除值(防止内存泄漏)
2.2 基本使用方式
// 创建ThreadLocal变量
private ThreadLocal<String> threadLocal = new ThreadLocal<>();// 设置值
threadLocal.set("线程特定的值");// 获取值
String value = threadLocal.get();// 使用完后移除值
threadLocal.remove();
3. ThreadLocal使用场景与实战
3.1 场景一:用户身份信息传递
在Web应用中,用户身份信息需要在同一个请求的不同组件、不同层之间传递,使用ThreadLocal可以避免在多个方法间传递用户对象。
实现步骤
1.创建用户上下文类
public class UserContext {private static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>();public static void setUser(User user) {USER_THREAD_LOCAL.set(user);}public static User getUser() {return USER_THREAD_LOCAL.get();}public static void clear() {USER_THREAD_LOCAL.remove();}
}
2.创建过滤器或拦截器来设置和清理用户信息
@Component
public class UserContextFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) request;try {// 从请求中获取用户信息(例如从JWT token中)User user = extractUserFromRequest(req);// 设置到ThreadLocal中UserContext.setUser(user);// 继续处理请求chain.doFilter(request, response);} finally {// 请求结束后清理ThreadLocal,避免内存泄漏UserContext.clear();}}private User extractUserFromRequest(HttpServletRequest request) {// 从请求头或Cookie中提取用户信息并创建User对象// 当然请求头和Cookie不一定非要带上完整的User信息,只要给一个能唯一锁定User的key就好// 这里省略具体实现...return new User();}
}
3.业务逻辑中使用
@Service
public class UserService {public void processUserRequest() {// 直接获取当前线程关联的用户信息User currentUser = UserContext.getUser();// 使用用户信息进行业务处理if (currentUser != null && currentUser.hasPermission("SOME_ACTION")) {// 执行需要权限的操作} else {throw new UnauthorizedException("No permission");}}
}
4.总结
实战中我们可以经常遇到这样的场景:请求实时获取当前发起请求用户个人信息。
其实就可以依据上述步骤来完成。
不可否认信息来源肯定还是来源于:token、header这类载体。常规处理方式,可能在解析到用户信息后放入缓存,或者实时获取。
但是这种处理方式是基于线程维度来处理,也就是说,在整个线程的生命周期中,所有上下文能够用到用户信息的地方都只需要经过一次解析后,随时随地从内存的ThreadLocalMap中拿到。
3.2 场景二:事务管理
在需要手动管理事务的场景中,可以使用ThreadLocal存储数据库连接。
实现步骤
1.创建数据库连接管理类
public class ConnectionManager {private static final ThreadLocal<Connection> CONN_HOLDER = new ThreadLocal<>();public static Connection getConnection() throws SQLException {Connection conn = CONN_HOLDER.get();if (conn == null) {// 创建新连接conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");// 设置为手动提交conn.setAutoCommit(false);// 存储到ThreadLocalCONN_HOLDER.set(conn);}return conn;}public static void beginTransaction() throws SQLException {Connection conn = getConnection();if (conn.getAutoCommit()) {conn.setAutoCommit(false);}}public static void commitTransaction() throws SQLException {Connection conn = CONN_HOLDER.get();if (conn != null) {conn.commit();}}public static void rollbackTransaction() throws SQLException {Connection conn = CONN_HOLDER.get();if (conn != null) {conn.rollback();}}public static void close() {Connection conn = CONN_HOLDER.get();if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}CONN_HOLDER.remove();}}
}
2.创建事务管理工具类
public class TransactionManager {public static void executeInTransaction(TransactionCallback callback) {try {// 开始事务ConnectionManager.beginTransaction();// 执行业务逻辑callback.execute();// 提交事务ConnectionManager.commitTransaction();} catch (Exception e) {// 发生异常时回滚try {ConnectionManager.rollbackTransaction();} catch (SQLException ex) {ex.printStackTrace();}throw new RuntimeException(e);} finally {// 关闭连接ConnectionManager.close();}}// 回调接口public interface TransactionCallback {void execute() throws Exception;}
}
3.在业务代码中使用
public class UserRepository {public void createUserWithAddress(User user, Address address) {TransactionManager.executeInTransaction(() -> {// 使用同一个连接执行多个SQLConnection conn = ConnectionManager.getConnection();// 插入用户try (PreparedStatement ps = conn.prepareStatement("INSERT INTO users (username, email) VALUES (?, ?)",Statement.RETURN_GENERATED_KEYS)) {ps.setString(1, user.getUsername());ps.setString(2, user.getEmail());ps.executeUpdate();// 获取生成的用户IDtry (ResultSet rs = ps.getGeneratedKeys()) {if (rs.next()) {int userId = rs.getInt(1);// 插入地址,关联用户IDtry (PreparedStatement addrPs = conn.prepareStatement("INSERT INTO addresses (user_id, street, city, country) VALUES (?, ?, ?, ?)")) {addrPs.setInt(1, userId);addrPs.setString(2, address.getStreet());addrPs.setString(3, address.getCity());addrPs.setString(4, address.getCountry());addrPs.executeUpdate();}}}}});}
}
4.总结
这种手动处理db链接的方式,其实也是借用了jdbc底层源码的套路,只不过在应用层做了一层拦截,强制让一个事务的sql群走一个链接。避免了手动另开事务进程,保证操作原子性。
能够有效避免数据库链接被占满的异常情况。
3.3 场景三:简化日期格式化
SimpleDateFormat
不是线程安全的,因为它的内部状态(如 Calendar
实例)会在多线程环境下被修改,导致数据错乱、异常或错误结果。
使用ThreadLocal可以为每个线程提供独立的实例。
实现步骤
1.创建日期工具类
public class DateUtil {// ThreadLocal存储每个线程的SimpleDateFormat实例private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));// 日期转字符串public static String formatDate(Date date) {return DATE_FORMATTER.get().format(date);}// 字符串转日期public static Date parseDate(String dateStr) throws ParseException {return DATE_FORMATTER.get().parse(dateStr);}// 修改日期格式public static void setDateFormat(String pattern) {DATE_FORMATTER.set(new SimpleDateFormat(pattern));}
}
2. 在多线程环境中使用
public class DateFormatExample {public static void main(String[] args) {// 创建线程池ExecutorService executor = Executors.newFixedThreadPool(10);for (int i = 0; i < 20; i++) {final int taskId = i;executor.submit(() -> {try {// 每个线程设置不同的日期格式if (taskId % 2 == 0) {DateUtil.setDateFormat("yyyy-MM-dd");} else {DateUtil.setDateFormat("MM/dd/yyyy HH:mm");}// 使用当前线程的格式化器Date now = new Date();String formattedDate = DateUtil.formatDate(now);System.out.println("Thread " + Thread.currentThread().getId() + " formatted date: " + formattedDate);// 解析日期Date parsedDate = DateUtil.parseDate(formattedDate);System.out.println("Thread " + Thread.currentThread().getId() + " parsed date: " + parsedDate);} catch (ParseException e) {e.printStackTrace();}});}executor.shutdown();}
}
3.4 场景四:追踪请求链路
在微服务架构中,需要追踪一个请求在不同服务间的调用链路,可以使用ThreadLocal来存储和传递追踪ID。
实现步骤
1.创建追踪上下文类
public class TraceContext {private static final ThreadLocal<String> TRACE_ID_HOLDER = new ThreadLocal<>();public static void setTraceId(String traceId) {TRACE_ID_HOLDER.set(traceId);}public static String getTraceId() {String traceId = TRACE_ID_HOLDER.get();// 如果不存在,则生成新的追踪IDif (traceId == null) {traceId = generateTraceId();TRACE_ID_HOLDER.set(traceId);}return traceId;}public static void clear() {TRACE_ID_HOLDER.remove();}// 生成唯一的追踪IDprivate static String generateTraceId() {return UUID.randomUUID().toString().replace("-", "");}
}
2.创建请求拦截器
@Component
public class TraceInterceptor implements HandlerInterceptor {private static final Logger logger = LoggerFactory.getLogger(TraceInterceptor.class);@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 尝试从请求头中获取追踪IDString traceId = request.getHeader("X-Trace-ID");// 如果没有,则生成新的if (traceId == null || traceId.isEmpty()) {traceId = TraceContext.getTraceId();} else {TraceContext.setTraceId(traceId);}logger.info("Processing request with trace ID: {}", traceId);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {// 请求完成后清理TraceContext.clear();}
}
3.配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate TraceInterceptor traceInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(traceInterceptor);}
}
4.创建日志工具类(可自定义)
这一步根据自己项目实际的需求来,这里只是简单举例,比如封装一个切面统一打印日志。
public class LogUtil {private static final Logger logger = LoggerFactory.getLogger(LogUtil.class);public static void info(String message) {logger.info("[TraceID: {}] {}", TraceContext.getTraceId(), message);}public static void error(String message, Throwable throwable) {logger.error("[TraceID: {}] {}", TraceContext.getTraceId(), message, throwable);}// 其他日志级别方法...
}
5.创建RestTemplate拦截器传递追踪ID
@Component
public class TraceRestTemplateInterceptor implements ClientHttpRequestInterceptor {@Overridepublic ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {// 获取当前线程的追踪ID,并添加到请求头request.getHeaders().add("X-Trace-ID", TraceContext.getTraceId());return execution.execute(request, body);}
}
6.配置RestTemplate
@Configuration
public class RestTemplateConfig {@Beanpublic RestTemplate restTemplate(TraceRestTemplateInterceptor traceInterceptor) {RestTemplate restTemplate = new RestTemplate();// 添加追踪拦截器restTemplate.setInterceptors(Collections.singletonList(traceInterceptor));return restTemplate;}
}
7. 在业务代码中使用
@Service
public class UserService {@Autowiredprivate RestTemplate restTemplate;public User getUserDetails(String userId) {LogUtil.info("Fetching user details for user: " + userId);// 调用用户服务User user = restTemplate.getForObject("/api/users/{id}", User.class, userId);LogUtil.info("Retrieved user: " + user.getUsername());// 调用订单服务List<Order> orders = restTemplate.getForObject("/api/orders?userId={id}", List.class, userId);LogUtil.info("Retrieved " + orders.size() + " orders for user");// 继续处理...return user;}
}
3.5 场景五:slf4j——MDC全局日志打印
MDC是slf4j提供的线程链路追踪的一种优雅实现,其实和上面那种方式差不多,只不过这里是作用于运行日志。
因为slf4j是日志门面框架,无论你的项目是使用logback还是log4j2来作为日志框架,最终的日志形式肯定会基于一个xml文件来约束。
MDC可以保存日志上下文,在打印日志时,打印出来这些上下文信息以上面那个3.4为基础,我们也可以在xml里配置每次都把traceId这个信息打印出来。
实现步骤
1.配置lombok(日志约束xml文件)
<configuration><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><!-- 确保包含 %X{traceId} --><pattern>[%thread] [traceId=%X{traceId}] %-5level %logger{36} - %msg%n</pattern></encoder></appender><root level="INFO"><appender-ref ref="STDOUT" /></root>
</configuration>
2.创建拦截器
public class HeaderInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 从 Header 中提取信息String authToken = request.getHeader("Authorization");String traceId = request.getHeader("X-Trace-Id");// 存储到 ThreadLocalif (authToken != null) {RequestContext.setAuthToken(authToken);}if (traceId != null) {RequestContext.setTraceId(traceId);MDC.put("traceId", traceId); 这里加上值}return true; // 继续执行后续拦截器和控制器}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {// 请求完成后清理 ThreadLocal,避免内存泄漏RequestContext.clear();}
}
3.配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate HeaderInterceptor traceInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(traceInterceptor);}
}
4.呈现效果
[http-nio-8081-exec-2] [traceId=123e4567-e89b-12d3-a456-426614174000] INFO org.example.service.WebService - get token:Basic dXNlcm5hbWU6cGFzc3dvcmQ=,traceId:123e4567-e89b-12d3-a456-426614174000
3.6 场景六:线程安全的数据缓存
在不同线程需要缓存不同数据的场景下,使用ThreadLocal可以避免同步问题。
其实这种场景很少,文章开头也说了ThreadLocal一般是不会用于线程同步的场景中,但是不代表ThreadLocal不能做。
线程同步的场景,一般都是使用异步编排、信号量、JUC工具类来解决的。
实现步骤
1.创建本地缓存类
public class LocalCache<K, V> {private final ThreadLocal<Map<K, V>> cache = ThreadLocal.withInitial(HashMap::new);public V get(K key) {return cache.get().get(key);}public void put(K key, V value) {cache.get().put(key, value);}public void remove(K key) {cache.get().remove(key);}public boolean containsKey(K key) {return cache.get().containsKey(key);}public void clear() {cache.get().clear();}// 完全清除ThreadLocalpublic void removeThreadLocal() {cache.remove();}
}
2.使用例子
public class ProductService {// 创建产品缓存private final LocalCache<String, Product> productCache = new LocalCache<>();public Product getProduct(String productId) {// 首先尝试从缓存获取Product product = productCache.get(productId);if (product == null) {// 缓存中不存在,从数据库加载product = loadProductFromDb(productId);// 放入缓存productCache.put(productId, product);}return product;}private Product loadProductFromDb(String productId) {// 从数据库加载产品(示例逻辑)try {// 模拟数据库访问延迟Thread.sleep(100);return new Product(productId, "Product " + productId, 99.99);} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("Loading product interrupted", e);}}// 在一个批处理任务结束时清理缓存public void cleanupCache() {productCache.removeThreadLocal();}
}
4. ThreadLocal实现继承性 - InheritableThreadLocal
每个线程栈都是独立私有的,普通的ThreadLocal无法将变量值从父线程传递到子线程,为解决这个问题,Java提供了InheritableThreadLocal。
4.1 基本用法
public class InheritableThreadLocalExample {// 创建InheritableThreadLocal变量private static final InheritableThreadLocal<String> CONTEXT = new InheritableThreadLocal<>();public static void main(String[] args) {// 在主线程中设置值CONTEXT.set("Main thread value");System.out.println("Main thread: " + CONTEXT.get());// 创建子线程Thread childThread = new Thread(() -> {// 子线程可以继承父线程的值System.out.println("Child thread: " + CONTEXT.get());// 子线程修改值不会影响父线程CONTEXT.set("Child thread value");System.out.println("Child thread after update: " + CONTEXT.get());});childThread.start();try {childThread.join();} catch (InterruptedException e) {e.printStackTrace();}// 主线程的值不受子线程修改的影响System.out.println("Main thread after child execution: " + CONTEXT.get());}
}
4.2 用于跨线程上下文传递
在复杂的异步调用场景中,使用InheritableThreadLocal传递上下文信息。
实现步骤
1.创建上下文类
public class ApplicationContext {private static final InheritableThreadLocal<Map<String, Object>> CONTEXT = new InheritableThreadLocal<Map<String, Object>>() {@Overrideprotected Map<String, Object> initialValue() {return new HashMap<>();}};public static void set(String key, Object value) {CONTEXT.get().put(key, value);}@SuppressWarnings("unchecked")public static <T> T get(String key) {return (T) CONTEXT.get().get(key);}public static void remove(String key) {CONTEXT.get().remove(key);}public static void clear() {CONTEXT.get().clear();}public static void removeThreadLocal() {CONTEXT.remove();}
}
2.创建异步执行器
@Configuration
public class AsyncConfig {@Beanpublic Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setQueueCapacity(25);executor.setThreadNamePrefix("AsyncTask-");// 创建自定义的TaskDecorator,用于传递ThreadLocalexecutor.setTaskDecorator(task -> {// 获取当前线程的上下文Map<String, Object> context = new HashMap<>(ApplicationContext.getAll());return () -> {// 在任务执行前,将上下文设置到新线程try {context.forEach(ApplicationContext::set);// 执行原始任务task.run();} finally {// 任务执行完毕,清理上下文ApplicationContext.clear();}};});return executor;}
}
3.在多线程环境中使用
@Service
public class AsyncService {@Async("taskExecutor")public CompletableFuture<String> processAsync(String input) {// 获取从调用线程继承的上下文String userId = ApplicationContext.get("userId");String traceId = ApplicationContext.get("traceId");System.out.println("Processing in async thread - UserId: " + userId + ", TraceId: " + traceId);// 处理业务逻辑try {Thread.sleep(1000); // 模拟耗时操作return CompletableFuture.completedFuture("Processed: " + input);} catch (InterruptedException e) {Thread.currentThread().interrupt();return CompletableFuture.failedFuture(e);}}
}
4.Controller引入
@RestController
@RequestMapping("/api")
public class AsyncController {@Autowiredprivate AsyncService asyncService;@GetMapping("/process")public CompletableFuture<String> process(@RequestParam String input, @RequestHeader("X-User-Id") String userId) {// 设置上下文信息ApplicationContext.set("userId", userId);ApplicationContext.set("traceId", UUID.randomUUID().toString());try {// 调用异步服务return asyncService.processAsync(input);} finally {// 清理主线程的上下文ApplicationContext.clear();}}
}
5. ThreadLocal常见问题与最佳实践
5.1 内存泄漏问题
ThreadLocal使用不当可能导致内存泄漏,因为ThreadLocalMap的Key是ThreadLocal的弱引用,而Value是强引用。
最佳实践
1.总是调用remove方法
在使用完ThreadLocal后调用remove()
方法
try {threadLocal.set(value);// 使用ThreadLocal变量的代码
} finally {threadLocal.remove();
}
2. 使用try-with-resources模式
创建AutoCloseable的ThreadLocal包装类
public class AutoCloseableThreadLocal<T> implements AutoCloseable {private final ThreadLocal<T> threadLocal = new ThreadLocal<>();public void set(T value) {threadLocal.set(value);}public T get() {return threadLocal.get();}@Overridepublic void close() {threadLocal.remove();}
}// 使用方式
try (AutoCloseableThreadLocal<String> local = new AutoCloseableThreadLocal<>()) {local.set("value");// 使用local变量
} // 自动调用close方法,清理ThreadLocal
5.2 父子线程值传递问题
如前所述,普通ThreadLocal无法将值从父线程传递到子线程。使用InheritableThreadLocal可解决这个问题,但它也有局限性:子线程只在创建时继承父线程的值,后续父线程的修改不会影响子线程。
对于线程池场景,可以使用阿里巴巴开源的TransmittableThreadLocal库。
使用TransmittableThreadLocal
1.依赖配置
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.14.2</version>
</dependency>
2.基本实践
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;public class TtlExample {private static final TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>();public static void main(String[] args) {// 创建线程池ExecutorService executor = Executors.newFixedThreadPool(2);// 设置主线程的值CONTEXT.set("Initial Value");// 提交任务executor.submit(TtlRunnable.get(() -> {System.out.println("Task 1: " + CONTEXT.get()); // 输出: Initial Value}));// 修改值CONTEXT.set("Updated Value");// 提交另一个任务executor.submit(TtlRunnable.get(() -> {System.out.println("Task 2: " + CONTEXT.get()); // 输出: Updated Value}));executor.shutdown();}
}
3.Spring集成
@Configuration
public class ThreadPoolConfig {@Beanpublic Executor asyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(10);executor.setMaxPoolSize(20);executor.setQueueCapacity(100);executor.setThreadNamePrefix("Async-");// 包装Executor,使其支持TransmittableThreadLocalexecutor.setTaskDecorator(runnable -> TtlRunnable.get(runnable));return executor;}
}
5.3 ThreadLocal的值修改
ThreadLocal存储的对象如果是可变的,其内部状态可能被意外修改,导致线程间的数据污染。
最佳实践
- 存储不可变对象:尽量存储String、Integer等不可变对象
- 使用深拷贝:如果必须存储可变对象,在get和set时进行深拷贝
public class SafeUserContextHolder {private static final ThreadLocal<User> USER_HOLDER = new ThreadLocal<>();// 存储用户时进行深拷贝public static void setUser(User user) {if (user == null) {USER_HOLDER.remove();} else {// 创建用户对象的副本User userCopy = new User();userCopy.setId(user.getId());userCopy.setUsername(user.getUsername());userCopy.setRoles(new ArrayList<>(user.getRoles()));USER_HOLDER.set(userCopy);}}// 获取用户时进行深拷贝public static User getUser() {User user = USER_HOLDER.get();if (user == null) {return null;}// 创建用户对象的副本User userCopy = new User();userCopy.setId(user.getId());userCopy.setUsername(user.getUsername());userCopy.setRoles(new ArrayList<>(user.getRoles()));return userCopy;}public static void clear() {USER_HOLDER.remove();}
}
5.4 初始化ThreadLocal值
可以通过覆盖initialValue()
方法或使用withInitial()
方法设置初始值。
// 方法1:覆盖initialValue方法
private static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};// 方法2:使用withInitial方法(Java 8+)
private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
6. 总结
ThreadLocal是一个强大的工具,适用于需要在同一线程内共享数据但又不希望进行线程同步的场景。它的主要应用场景包括:
- 用户身份信息传递:在Web应用中传递用户上下文
- 事务管理:存储和传递数据库连接
- 线程安全的对象:为每个线程提供专用的非线程安全对象
- 请求链路追踪:在分布式系统中追踪请求
注意事项:
- 及时清理:使用完毕后调用remove()方法,避免内存泄漏
- 合理封装:将ThreadLocal的操作封装在统一的上下文管理类中
- 注意线程复用:在线程池环境中要特别小心ThreadLocal的使用
- 数据隔离:清楚认识ThreadLocal是为了隔离线程数据,而非共享数据