目录
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 {
@Override
public 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);
// 存储到ThreadLocal
CONN_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(() -> {
// 使用同一个连接执行多个SQL
Connection 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();
// 获取生成的用户ID
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
int userId = rs.getInt(1);
// 插入地址,关联用户ID
try (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();
// 如果不存在,则生成新的追踪ID
if (traceId == null) {
traceId = generateTraceId();
TRACE_ID_HOLDER.set(traceId);
}
return traceId;
}
public static void clear() {
TRACE_ID_HOLDER.remove();
}
// 生成唯一的追踪ID
private static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
}
2.创建请求拦截器
@Component
public class TraceInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(TraceInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 尝试从请求头中获取追踪ID
String 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;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 请求完成后清理
TraceContext.clear();
}
}
3.配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TraceInterceptor traceInterceptor;
@Override
public 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 {
@Override
public 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 {
@Bean
public RestTemplate restTemplate(TraceRestTemplateInterceptor traceInterceptor) {
RestTemplate restTemplate = new RestTemplate();
// 添加追踪拦截器
restTemplate.setInterceptors(Collections.singletonList(traceInterceptor));
return restTemplate;
}
}
7. 在业务代码中使用
@Service
public class UserService {
@Autowired
private 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 {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从 Header 中提取信息
String authToken = request.getHeader("Authorization");
String traceId = request.getHeader("X-Trace-Id");
// 存储到 ThreadLocal
if (authToken != null) {
RequestContext.setAuthToken(authToken);
}
if (traceId != null) {
RequestContext.setTraceId(traceId);
MDC.put("traceId", traceId); 这里加上值
}
return true; // 继续执行后续拦截器和控制器
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 请求完成后清理 ThreadLocal,避免内存泄漏
RequestContext.clear();
}
}
3.配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private HeaderInterceptor traceInterceptor;
@Override
public 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();
}
// 完全清除ThreadLocal
public 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>>() {
@Override
protected 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 {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("AsyncTask-");
// 创建自定义的TaskDecorator,用于传递ThreadLocal
executor.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 {
@Autowired
private 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();
}
@Override
public 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 {
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Async-");
// 包装Executor,使其支持TransmittableThreadLocal
executor.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>() {
@Override
protected 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是为了隔离线程数据,而非共享数据