一、前言
在实际工作中,若用到多线程,就会遇到共享变量的问题,也就是在不同的线程间共享变量的值,这个时候就会用到同步机制。比如synchronized关键字或锁对象。这些机制确保了在任何时候只有一个线程能够访问共享资源,防止了并发访问时可能出现的冲突。
然而,ThreadLocal类提供了一种不同的方法来处理线程安全问题。ThreadLocal允许我们创建仅对创建它们的线程可见的变量。这意味着每个线程都有自己的变量副本,不会与其他线程发生冲突。下面我们一起从它的定义、使用场景、优缺点、注意事项、原理分析等方面了解ThreadLocal。
二、ThreadLocal 是什么?
ThreadLocal 是 java.lang包中的一个类,用于创建线程局部变量。什么意思呢?就是说它是每个线程自己的,私有的,不同的线程访问这个变量时,获取到的是自己线程的副本,大家互不影响。
1.ThreadLocal 类的用途和特点
- 线程局部性: ThreadLocal 允许每个线程拥有变量的独立副本,这意味着同一变量在不同的线程中的值可以互不影响
- 通常作为私有字段: ThreadLocal 通常作为类的私有静态成员,以便将状态与线程信息关联起来
- 内存管理: 每个线程隐式持有对其线程局部变量的引用,只要线程存货且ThreadLocal 实列可访问,线程结束后,在没有其他引用的情况下所有线程局部变量的副本都将成为垃圾收集器的候选对象。
2.ThreadLocal 类的构造函数和方法
- 构造函数
- ThreadLocal():创建一个线程局部变量
- 方法
- get():返回当前线程的线程局部变量的值
- set(T value): 设置当前线程的线程局部变量的副本为指定的值
- remove(): 移除当前线程的线程局部变量的值
- initialValue():返回当前线程的线程局部变量的“初始值”。这个方法将在第一次通过get()方法访问变量时被调用,除非线程之前调用了set()方法。
- withInitial(Supplier<? extends T> supplier):创建一个线程局部变量,变量的初始值由调用Supplier的get方法确定。
get():返回当前线程的线程局部变量的值。
set(T value):设置当前线程的线程局部变量的副本为指定的值。
remove():移除当前线程的线程局部变量的值。
initialValue():返回当前线程的线程局部变量的“初始值”。这个方法将在第一次通过get()方法访问变量时被调用,除非线程之前调用了set()方法。
withInitial(Supplier<? extends T> supplier):创建一个线程局部变量,变量的初始值由调用Supplier的get方法确定。
三、ThreadLocal 解决了什么问题?
ThreadLocal 主要解决了多线程环境下共享变量的线程安全问题。通过将变量设置为 ThreadLocal 类型,每个线程都拥有自己的变量副本,从而保证了线程间的数据隔离,避免了竞态条件的发生。
四、使用场景
- 用户会话信息:在Web应用中,每个用户的会话信息可以通过ThreadLocal存储,避免不同用户间的会话信息相互干扰
- 数据库连接管理:每个线程可以在自己的ThreadLocal中保存数据库连接,这样就不需要每次操作数据库时都创建信的连接。
- 计数器和局部变量:在多线程环境下,需要对某些数据进行计数或者维护一些临时变量时,可以使用ThreadLocal来避免线程间的数据干扰。
五、使用注意事项
-
内存泄漏:如果ThreadLocal的变量没有在不需要时及时清理,可能会导致内存泄漏。因此,使用ThreadLocal后,应当在适当的时候调用remove()方法来清除数据。
-
线程池使用:在使用线程池时,线程会被重用,如果不正确管理ThreadLocal变量,可能会导致数据混乱。可以通过自定义Thread类或者使用InheritableThreadLocal来解决这个问题。
-
非静态内部类:ThreadLocal通常作为非静态内部类使用,以确保每个线程都有自己的实例
六、示例介绍
以下就以用户会话信息 作为示例
首先,我们定义了一个CurrentUserContext类,它使用ThreadLocal来存储当前用户的上下文信息。这个类是无状态的,并且设计为单例模式,通过静态方法来操作当前线程的ThreadLocal变量。
在这个类中,我们定义了一个静态的ThreadLocal变量LOCAL,它将存储CurrentUser对象。CurrentUser是一个简单的POJO,用来表示当前用户的信息,比如用户ID、邮箱等。
/**
* 用于全局获取 CurrentUser <br/>
* 通过 {@link CurrentUserContextInterceptor } 添加上下文
*
*/
@UtilityClass
public class CurrentUserContext {
// 线程局部变量
private static final ThreadLocal<CurrentUser> LOCAL = new ThreadLocal<>();
public static void add(CurrentUser user) {
LOCAL.set(user);
}
public static void clear() {
LOCAL.remove();
}
public static CurrentUser getCurrentUser() {
return LOCAL.get();
}
}
/**
* user 对象信息
*/
@Data
publica class CurrentUser {
private String userId;
private String userType;
private String userEmail;
private String userName;
}
接下来,我们创建了一个CurrentUserContextInterceptor类,它实现了HandlerInterceptor接口,用于在Spring MVC的请求处理流程中拦截请求。这个拦截器的主要任务是解析HTTP请求头中的用户信息,并将其存储到CurrentUserContext中。
/**
* 拦截当前Http请求,从当前http请求中获取当前用户相关的Header,组装CurrentUser对象,存到{@link CurrentUserContext }中,方便全局获取
*
*/
@Slf4j
public class CurrentUserContextInterceptor implements HandlerInterceptor {
/**
* 默认用户
*/
private static final CurrentUser DEFAULT_USER = new CurrentUser(
"0",
"",
"RicardoYHu@163.com",
""
);
/**
* HTTP 编码
*/
private static final String ENC = "utf-8";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
buildUserByApiHeader(request);
} catch (Exception ex) {
// 若request header中没有user信息,则添加默认用户
CurrentUserContext.add(DEFAULT_USER);
}
return true;
}
private void addToContext(CurrentUser user) {
if (StringUtils.isNotBlank(user.getUserEmail())) {
// 记录真实的用户信息
CurrentUserContext.add(user);
} else {
CurrentUserContext.add(DEFAULT_USER);
}
}
/**
* API请求
*
* @param request
*/
private void buildUserByApiHeader(HttpServletRequest request) {
CurrentUser user = new CurrentUser();
// 其中 BusinessPropertyEnum 是业务属性枚举
user.setUserId(UriUtil.safeDecode(request.getHeader(BusinessPropertyEnum.CURRENT_USER_ID.getApiHeaderName()), ENC));
user.setUserType(UriUtil.safeDecode(request.getHeader(BusinessPropertyEnum.CURRENT_USER_TYPE.getApiHeaderName()), ENC));
user.setUserEmail(UriUtil.safeDecode(request.getHeader(BusinessPropertyEnum.CURRENT_USER_EMAIL.getApiHeaderName()), ENC));
user.setSellerId(UriUtil.safeDecode(request.getHeader(BusinessPropertyEnum.CURRENT_USER_SELLER.getApiHeaderName()), ENC));
addToContext(user);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清空上下文对象,防止内存泄露
CurrentUserContext.clear();
}
}
在preHandle方法中,解析API请求头。我们都将解析出的用户信息存储到CurrentUserContext中。
如果在解析过程中出现异常,我们捕获异常并设置一个默认用户,以确保不会因为用户信息的缺失而影响程序的正常运行。
最后,在afterCompletion方法中,我们清理CurrentUserContext,移除存储的CurrentUser对象。这是一个非常重要的步骤,因为它防止了内存泄漏。在Web应用中,每个请求都是短暂的,请求结束后不应该再保留任何请求相关的数据。通过调用clear方法,我们确保了每次请求结束后,ThreadLocal中存储的用户信息都会被清除。
通过这种方式,我们可以在整个请求处理过程中轻松访问当前用户的信息,而无需担心线程安全问题。ThreadLocal为我们提供了一种简单而有效的方法来处理多线程环境下的上下文信息管理。
七、底层原理
ThreadLocal的底层原理其实很简单。在Java中,每个线程都是Thread类的一个实例。ThreadLocal类使用了一个内部的ThreadLocalMap来存储每个线程的变量副本。这个映射表以线程对象为键,以线程局部变量为值。
当线程调用ThreadLocal的set()方法时,它会在自己的Thread对象上找到对应的ThreadLocalMap,并将变量存储在这个映射表中。当线程调用get()方法时,ThreadLocal会在当前线程的ThreadLocalMap中查找并返回对应的变量。
通过这种方式,ThreadLocal确保了每个线程都可以独立地存储和访问自己的变量,而不会与其他线程冲突。这样,即使在多线程环境下,每个线程也能安全地管理自己的数据。