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是为了隔离线程数据,而非共享数据
相关文章:

ThreadLocal详解与实战指南
目录 1. ThreadLocal基本概念 1.1 核心原理 1.2 主要特性 2. ThreadLocal API详解 2.1 核心方法 2.2 基本使用方式 3. ThreadLocal使用场景与实战 3.1 场景一:用户身份信息传递 实现步骤 1.创建用户上下文类 2.创建过滤器或拦截器来设置和清理用户信息 …...
[FPGA基础] RAM篇
Xilinx FPGA RAM 使用指南 1. 引言 随机存取存储器(RAM)是 Xilinx FPGA 设计中用于存储和快速访问数据的重要资源。Xilinx FPGA 提供多种 RAM 类型,包括块 RAM(Block RAM)和分布式 RAM(Distributed RAM&a…...

【含文档+PPT+源码】基于微信小程序的校园快递平台
项目介绍 本课程演示的是一款基于微信小程序的校园快递平台,主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 1.包含:项目源码、项目文档、数据库脚本、软件工具等所有资料 2.带你从零开始部署运行本套系统 3.该项目附带…...

【CODEMATE】进制转换(transform) 粤港澳青少年信息学创新大赛 C/C++/Python 解题思路
目录 问题描述做题思路,解决过程思路:踩过的坑:核心代码C 语言 / C 切片:C 语言 / C 判断 ‘A’ 数量:Python 切片:Python 判断 ‘A’ 数量: 完整代码C 语言 完整代码C 完整代码Python 完整代码…...
统计文件中单词出现的次数并累计
# 统计单词出现次数 fileopen("E:\Dasktape/python_test.txt","r",encoding"UTF-8") f1file.read() # 读取文件 countf1.count("is") # 统计文件中is 单词出现的次数 print(f"此文件中单词is出现了{count}次")# 2.判断单词出…...
DbCreateHelper数据库创建指南
DbCreateHelper 是一个用于简化数据库创建和初始化过程的工具类,它封装了常见的数据库操作,使开发者能够更便捷地创建、配置和管理数据库。 1.主要功能 数据库创建与初始化 表结构创建与管理 数据库版本控制 数据迁移支持 数据库连接管理 基本用法…...

2025 Java 开发避坑指南:如何避免踩依赖管理的坑?
在 Java 开发的世界里,依赖管理就像是一座看不见的桥梁,连接着项目所需的各种第三方库和框架。然而,这座桥梁并非总是稳固,稍有不慎就可能掉入 “依赖地狱”,导致项目编译失败、运行异常。2025 年,随着开源…...

ARM服务器与X86服务器核心区别分析
ARM服务器与X86服务器核心区别分析 一、架构设计与指令集差异 指令集本质 ARM:基于RISC(精简指令集),指令定长且简单,单周期执行效率高,硬件设计复杂度低,适合低功耗场景。 X86…...

人口老龄化丨AI健康小屋如何实现防病于未然
随着全球老龄化加剧,“银发浪潮” 对医疗资源、养老护理和健康管理提出了严峻挑战。 由此智绅科技应运而生,七彩喜智慧养老系统构筑居家养老安全网。 AI 健康小屋作为银发科技的创新载体,通过智能化健康监测、精准化风险预警、便捷化医疗衔…...

记录搭建自己应用中心
记录搭建自己应用中心 应用架构主应用-管理中心系统文件系统子应用 日志系统日志系统前端日志系统后端 用户系统接入使用暂未完成 研发管理需求面板消息推送任务分配应用发布 应用架构 一直想做个试试,这是一个简易版的,主要是整合下知识的,…...

git版本回退 | 远程仓库的回退 (附实战Demo)
目录 前言1. 基本知识2. Demo3. 彩蛋 前言 🤟 找工作,来万码优才:👉 #小程序://万码优才/r6rqmzDaXpYkJZF 爬虫神器,无代码爬取,就来:bright.cn 本身暂存区有多个文件,但手快了&…...

STM32 的 GPIO和中断
GPIO的简单介绍 内部结构 施密特触发器(TTL肖特基触发器) 的工作原理: 施密特触发电路(简称)是一种波形整形电路,当任何波形的信号进入电路时,输出在正、负饱和之间跳动,产生方波或…...
【Python数据库编程实战】从SQL到ORM的完整指南
目录 前言技术背景与价值当前技术痛点解决方案概述目标读者说明 一、技术原理剖析核心概念图解核心作用讲解关键技术模块说明技术选型对比 二、实战演示环境配置要求核心代码实现案例1:SQLite基础操作案例2:MySQL连接池案例3:SQLAlchemy ORM …...

【因果推断】(二)CV中的应用
文章目录 因果表征学习因果图 (Causal Diagram)“后门准则”(backdoor criterion)和“前门准则”(frontdoor criterion)后门调整Visual Commonsense R-CNNCausal Intervention for Weakly-Supervised Semantic SegmentationCausal…...

分享Matlab成功安装Support Package硬件支持包的方法
分享Matlab成功安装Support Package硬件支持包的方法 文章目录 分享Matlab成功安装Support Package硬件支持包的方法一、 引言二、 操作步骤三、 附件资料四、总结 一、 引言 最近,我想学习基于Matlab simscape & Arduino实现硬件在环仿真,其中物理…...

电子级甲基氯硅烷
电子级甲基氯硅烷是一类高纯度有机硅化合物,主要用于半导体制造、光伏产业及高端电子材料领域。以下从技术特性、应用场景、生产工艺、市场动态及安全规范等方面展开分析: 一、核心特性与技术标准 高纯度要求 电子级甲基氯硅烷的纯度通常需达到99.99% 以…...
C语言面试高频题——define 和typedef 的区别?
1. 基本概念 (1) #define 定义:#define 是预处理指令,用于定义宏。作用:在编译之前进行文本替换。语法:#define 宏名 替换内容示例:#define PI 3.14159 #define SQUARE(x) ((x) * (x))(2) typedef 定义:…...
[FPGA基础] 原语简介篇
Xilinx FPGA 原语介绍指南 1. 引言 在 Xilinx FPGA 设计中,原语 (Primitives) 是 FPGA 硬件资源的最基本构建块,直接映射到 FPGA 的底层硬件结构。原语允许设计者以低级别方式控制硬件行为,优化性能、功耗和资源利用率。本文档基于 Xilinx …...

【金仓数据库征文】- 深耕国产数据库优化,筑牢用户体验新高度
目录 引言 一、性能优化:突破数据处理极限,提升运行效率 1.1 智能查询优化器:精准优化数据检索路径 1.2 并行处理技术:充分释放多核计算潜力 1.3 智能缓存机制:加速数据访问速度 二、稳定性提升:筑牢…...

热度大幅度下降,25西电经济与管理学院(考研录取情况)
1、经济与管理学院各个方向 2、经济与管理学院近三年复试分数线对比 学长、学姐分析 由表可看出: 1、应用经济及学25年相较于24年下降25分,为325分 2、管理科学与工程25年相较于24年保持不变,为375分 3、工商管理学25年相较于24年下降5分…...
安卓四大组件之ContentProvider
目录 实现步骤 代码分析 onCreate insert query ContextHolder Cursor 作用与用法 基本步骤: 可能的面试题:为什么使用Cursor? 为什么使用Cursor 使用Cursor的好处 静态内部类实现单例模式 AnndroidManifest.xml配置信息 注释的…...

DeepSeek+Mermaid:轻松实现可视化图表自动化生成(附实战演练)
目录 一、引言:AI 与图表的梦幻联动二、DeepSeek:大语言模型新星崛起2.1 DeepSeek 全面剖析2.2 多场景应用示例2.2.1 文本生成2.2.2 代码编写 三、Mermaid:代码式图表绘制专家3.1 Mermaid 基础探秘3.2 语法与图表类型详解3.2.1 流程图&#x…...
redis客户端库redis++在嵌入式Linux下的交叉编译及使用
在开发过程中,我们经常会遇到需要在嵌入式Linux系统上与Redis进行交互的需求。因此选择一个适合的Redis客户端库就显得尤为重要。下面介绍下c中有名的redis-plus-plus(redis)三方库在嵌入式linux下的交叉编译及使用。该库底层是基于hiredis的…...
gitlab-ce容器镜像源(国内)
下载命令 docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/gitlab/gitlab-ce:17.10.4-ce.0 更多参考: https://docker.aityp.com/image/docker.io/gitlab/gitlab-ce:17.10.4-ce.0...
小白自学python第二天
学习python的第二天 一、判断语句 1、布尔类型和比较运算符 1、布尔类型 表示现实生活中的逻辑,真(True,用数字1表示)和假(False,用数字0表示) 2、布尔类型变量的定义 变量的名称 布尔类…...
嵌入式面试高频考点深度解析:内存管理、指针操作与结构体实战指南
试题一:大小端系统中数据的内存表现形式 题目 short tmp 0xaabb; 请分别写出大小端系统中,tmp 在内存中的表现形式。 分析 1. 什么是高位与低位? 对于一个数据而言,以十六进制数 0xaabb 为例,从左至右࿰…...
ApplicationRunner的run方法与@PostConstruct注解
ApplicationRunner 的 run 方法与 PostConstruct 注解在 Spring Boot 中均用于初始化逻辑,但二者的 执行时机、作用范围 和 功能特性 存在显著差异。以下是详细对比分析: 一、核心差异对比 维度PostConstructApplicationRunner.run()触发时机Bean 实例化…...

今日行情明日机会——20250425
指数依然在震荡,等待方向选择,整体量能不搞但个股红多绿少。 2025年4月25日涨停板行业方向分析如下: 一、核心行业方向及驱动逻辑 一季报增长(17家涨停) 核心个股:惠而浦、鸿博股份、卫星化学驱动逻辑&am…...

一道MySQL索引题
复合索引基础 MySQL中的复合索引(Composite Index)是指由多个列组成的索引。与单列索引不同、复合索引的结构更为复杂,但使用得当可以大幅提升查询性能。 复合索引的工作原理 复合索引的本质是一种有序的数据结、每个列是建立在那个索引前一列存在的情况下、那一…...
Python 读取 txt 文件详解 with ... open()
文章目录 1 概述1.1 注意事项1.2 模式说明1.3 文件准备 2 读文件2.1 读取整个文件2.2 逐行读取2.3 读取所有行到列表 3 写文件3.1 覆盖写入3.2 追加写入3.3 写入多行 4 实用技巧4.1 检查文件是否存在4.2 异常处理 1 概述 1.1 注意事项 文件编码:建议指定编码&…...