目录
一、ThreadLocal的原理
1、ThreadLocal对象
2、ThreadLocalMap
3、Thread 对象
4、get() 和 set() 方法
5、内存管理
二、ThreadLcoal的应用
三、ThreadLocal扩展问题
四、总结
ThreadLocal 类在 Java 中提供了一种机制,可以在每个线程中存储独立的变量副本,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用。避免了多线程环境下的数据共享问题
//创建一个ThreadLocal变量
private static final ThreadLocal<String> requestContext = new ThreadLocal<>();
并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现线性安全问题。
为了解决线性安全问题,可以用加锁的方式,比如使用synchronized
或者Lock
。但是加锁的方式,可能会导致系统变慢,也可以使用volatile
一、ThreadLocal的原理
先了解一下ThreadLocal的结构图:
再瞅一眼ThreadLocal的源码结构:
public class ThreadLocal<T> {
...
public T get() {
...
}
//用于设置 ThreadLocal 的初始值
private T setInitialValue() {
//获取要初始化的数据
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为空
if (map != null)
//将初始值设置到map中,key是this,即threadLocal对象,value是初始值
map.set(this, value);
else
//如果map为空,则需要创建新的map对象
createMap(t, value);
return value;
}
public void set(T value) {
...
}
static class ThreadLocalMap {
...
}
...
}
再一个个细看内部元素的含义:
1、ThreadLocal对象
ThreadLocal 是一个类,它定义了如何为每个线程创建和存储变量副本的方法。
当创建一个 ThreadLocal 对象时,它本身并不存储任何数据,而是为每个使用它的线程提供了一个独立的数据槽。
2、ThreadLocalMap
在源码中,有一个静态的内部类叫:ThreadLocalMap
,它存储了该线程的所有 ThreadLocal 变量的副本
它是一个哈希表,用于存储键值对,其中键是 ThreadLocal 对象,值是线程的局部变量副本
也就是一个Entry
数组,其中:Entry
= ThreadLocal
+ value
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
private Entry[] table;
...
}
3、Thread 对象
之前说了ThreadLocal为每个线程提供了本地变量,那Thread中肯定是有ThreadLocal的,见Thread源码:
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
}
由此可见:在每个Thread
类中,都有一个ThreadLocalMap
的成员变量,该变量包含了一个Entry数组
,该数组真正保存了ThreadLocal类set的数据
4、get() 和 set() 方法
ThreadLocal 提供了 get() 方法来获取当前线程的局部变量副本,set() 方法用于设置当前线程的局部变量副本的值
public class ThreadLocal<T> {
...
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//根据threadLocal对象从map中获取Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获取保存的数据
T result = (T)e.value;
return result;
}
}
//初始化数据
return setInitialValue();
}
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为空
if (map != null)
//将值设置到map中,key是this,即threadLocal对象,value是传入的value值
map.set(this, value);
else
//如果map为空,则需要创建新的map对象
createMap(t, value);
}
...
}
5、内存管理
ThreadLocalMap 使用 WeakReference 作为键来存储 ThreadLocal 对象,ThreadLocal对象是弱引用,
以防止内存泄漏。
如果 ThreadLocal 对象没有被引用,在GC
的时候,那么它会被垃圾回收器回收,而 ThreadLocalMap 中对应的条目也会被自动清理
总结一下引用关系:
除了Entry的key对ThreadLocal对象是弱引用
,其他的引用都是强引用
到这里应该了解了一点ThreadLocal,那有几个问题需要思考:
❓1:为什么用ThreadLocal做key,而不是用Thread
做key?
一个线程中只使用了一个ThreadLocal
对象,但是一个线程未必只有一个ThreadLocal
对象
每个 ThreadLocal 对象都是唯一的,因此可以用作键来区分不同的 ThreadLocal 实例
这种唯一性保证了每个 ThreadLocal 对应的变量副本可以在 ThreadLocalMap 中正确地存储和检索
❓2:为什么ThreadLocal对象是弱引用,而不是强引用?
先了解一下弱引用 和 强引用
- 强引用:最常用的引用类型,只要还有强引用指向一个对象,垃圾收集器就不会回收这个对象。
- 弱引用:只能通过 WeakReference 类来创建,当 JVM 进行垃圾回收并且需要更多内存时,即使系统中还有足够的内存,弱引用关联的对象也会被回收
从流程图和源码可以知道,Thread中的ThreadLocal变量对ThreadLocal对象是有强引用存在的
即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用
此时,如果执行该代码的线程
使用了线程池
,一直长期存在,不会被销毁
就会存在这样的强引用链
:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象
那么,ThreadLocal对象和ThreadLocalMap都将不会被GC
回收,于是产生了内存泄露
问题
因此,ThreadLocal对象是弱引用
二、ThreadLcoal的应用
乍一看,volatile和ThreadLocal有一点类似,都是为了解决并发场景下修改一个共享变量的问题,那为什么还需要用ThreadLocal呢?
在之前的文章Java并发—volatile关键字的作用及使用场景-CSDN博客中,已经详细讲了volatile了
还是使用之前的例子🌰:线程A和线程B从主内存读取和修改x=1的过程:
- 初始化:创建一个 ThreadLocal 对象,并通过它为每个线程分配一个独立的副本。
- 读取:线程 A 和 B 分别从各自的 ThreadLocal 实例中读取 x 的值。
- 修改:线程 A 修改 x 的值时,实际上是在修改自己线程内的副本。
- 可见性:线程 B 读取 x 的值时,只能看到自己线程内的副本,不会看到线程 A 的修改。
示例代码
下面是一个简单的示例,展示了使用 ThreadLocal 如何为每个线程提供独立的变量副本:
public class VolatileExample {
private static volatile int x = 1;
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
System.out.println("Thread A: Before modification, x = " + x);
x = 3; // 修改 x 的值
System.out.println("Thread A: After modification, x = " + x);
});
executorService.submit(() -> {
System.out.println("Thread B: Before modification, x = " + x);
x = 4; // 修改 x 的值
System.out.println("Thread B: After modification, x = " + x);
});
executorService.shutdown();
}
}
运行结果:
Thread A: Before modification, x = 1
Thread A: After modification, x = 3
Thread B: Before modification, x = 1
Thread B: After modification, x = 4
- 线程A:从主内存读取到x=1,修改到x=3
- 线程B:从主内存读取到x=1,修改到x=4
- 最终结果:
- 线程A的副本中的 x 值为 3。
- 线程B的副本中的 x 值为 4。
- 主内存中的 x 值仍然为 1,因为 ThreadLocal 并不修改主内存中的值
从运行结果可见:
使用 ThreadLocal 时,每个线程都有一个独立的变量副本,因此线程 A 和 B 读取和修改的实际上是它们自己的副本,而不是共享的主内存中的 x。这种方式避免了线程间的竞争和同步问题,同时也意味着线程 A 对 x 的修改不会影响到线程 B
而使用 volatile 时,线程 A 和线程 B 读取和修改的是同一个共享变量 x
ThreadLocal和volatile的区别:
- 目的不同:volatile 解决的是变量在多线程间的可见性问题,而 ThreadLocal 解决的是线程间的数据隔离问题。
- 应用场景不同:volatile 适用于状态标记和简单的原子性操作,而 ThreadLocal 适合于存储每个线程独享的资源或状态信息。
- 数据共享与否:volatile 变量在所有线程中共享,而 ThreadLocal 中的变量对每个线程来说是独立的
ThreadLocal 是一种非常有用的工具,它可以让每个线程拥有自己的变量副本,从而避免了线程间的共享状态问题,那么ThreadLocal的应用场景:
1、每个线程独享的对象
工具类:例如 SimpleDateFormat 和 Random 等类,这些类在多线程环境下可能会产生线程安全问题。使用 ThreadLocal 可以为每个线程提供独立的实例。
线程池中的工具类:在使用线程池时,确保每个线程都有自己的工具类实例,避免线程间的数据竞争
2、保存线程内的全局变量
拦截器中的用户信息:在 Web 开发中,可以在拦截器中获取用户的认证信息(如用户名、用户ID等),并将其存储在 ThreadLocal 中,以便后续处理中可以直接使用,避免了参数传递的麻烦。
请求上下文信息:在 Web 请求处理过程中,可以将请求相关的上下文信息(如请求ID、事务ID等)存储在 ThreadLocal 中,方便日志记录和跟踪
🌰: ThreadLocal来存储和访问当前登录用户信息
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
public class UserContextHolder {
// 创建一个 ThreadLocal 变量来存储 UserInfo
private static final ThreadLocal<UserInfo> userInfoHolder = new ThreadLocal<>();
/**
* 设置用户信息
*/
public static void setUserInfo(UserInfo userInfo) {
userInfoHolder.set(userInfo);
}
/**
* 获取当前线程的用户信息
*/
public static UserInfo getUserInfo() {
UserInfo userInfo = userInfoHolder.get();
if (userInfo == null) {
throw new EcholaRuntimeException(SysExceptionEnum.UNAUTHORIZED);
}
return userInfo;
}
/**
* 移除用户信息
*/
public static void removeRequestId() {
userInfoHolder.remove();
}
}
在拦截器 (HandlerInterceptor) 中处理 ThreadLocal 的设置和清除,将并用户信息将其设置到 ThreadLocal 中,以确保每个请求都有独立的上下文
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求中获取用户信息
// 在请求处理之前调用,用于从请求中提取用户信息,并将其设置到 ThreadLocal 中
UserInfo userInfo = extractUserInfoFromRequest(request);
// 设置用户信息到 ThreadLocal
UserInfo.setUserInfo(userInfo);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 这里可以做些额外的处理,例如添加模型数据
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求处理完成后,清除 ThreadLocal
UserInfo.remove();
}
/**
* 从请求中提取用户信息
*
* @param request 请求对象
* @return 用户信息
*/
private UserInfo extractUserInfoFromRequest(HttpServletRequest request) {
// 假设用户信息存储在请求头中
String userId = request.getHeader("X-User-ID");
if (userId == null) {
throw new LzmRuntimeException(SysExceptionEnum.UNAUTHORIZED);
}
// 创建 UserInfo 对象
UserInfo userInfo = new UserInfo();
userInfo.setId(Integer.parseInt(userId));
// ... 其他字段的设置
return userInfo;
}
}
这样在处理请求的过程中,无论在哪一层代码中,都可以通过 getUserInfo 方法来获取当前登录用户信息
UserInfo userInfo = UserContextHolder.getUserInfo();
通过这种方式,每个请求都有独立的用户信息上下文,保证了线程之间的数据隔离,同时也确保了资源的正确释放
3、替代参数传递
避免参数传递:在多个方法之间传递相同的参数时,可以使用 ThreadLocal 来存储这些参数,从而避免了在方法签名中显式传递这些参数。
日志记录:在日志记录中,可以使用 ThreadLocal 来存储一些上下文信息(如请求ID、用户ID等),以便在日志中记录这些信息
4、线程池中的上下文管理
线程池中的线程上下文:在使用线程池时,可以使用 ThreadLocal 来存储每个线程的上下文信息,如线程标识、配置信息等
5、数据库连接管理
线程本地的数据库连接:在多线程环境中,为了提高性能,可以使用 ThreadLocal 来管理每个线程的数据库连接,避免了连接的频繁创建和销毁
三、ThreadLocal扩展问题
然而,在使用线程池时,由于线程池中的线程会被复用,因此直接使用 ThreadLocal 可能会导致一些问题,比如数据污染或内存泄漏
❓:那么如何在线程池中共享数据?
1、 使用 InheritableThreadLocal(可继承的ThreadLocal)
InheritableThreadLocal 是 ThreadLocal 的一个子类,它允许子线程继承父线程的 ThreadLocal 值。虽然这在某些情况下有用,但它不适合线程池,因为线程池中的线程通常是复用的,而且通常不会有父子线程关系
2、在线程池的 ThreadFactory 中初始化 ThreadLocal
可以通过覆盖线程池的 ThreadFactory 来为每个新创建的线程初始化 ThreadLocal 的值。这种方式确保每个线程在其生命周期开始时都会获得一个新的初始值
四、总结
ThreadLocal 是 Java 中的一种特殊机制,它为每个线程提供了一个独立的变量副本,主要用于解决多线程环境下的线程安全问题
通过 ThreadLocal,每个线程可以拥有自己的变量副本,从而避免了线程间的数据竞争和同步问题