ThreadLocal 介绍与简单使用

HYF Lv3

线程封闭是一种通过限制数据的访问范围来实现线程安全的技术。其核心思想是将数据限制在单个线程的上下文中,使得其他线程无法直接访问或修改这些数据。这种方法避免了线程间的数据竞争和同步问题。ThreadLocal 是线程封闭的一种实现。

什么是 ThreadLocal

ThreadLocalJava 提供的一种线程局部变量,用于在多线程环境中为每个线程提供独立的变量副本。它的工作机制可以简单描述为:每个线程都拥有一个独立的 ThreadLocal 变量副本:当一个线程访问 ThreadLocal 变量时,它实际上访问的是该线程自己持有的副本,而不是其他线程共享的变量。这种机制确保了线程间的数据隔离和线程安全。

ThreadLocal 实现细节

ThreadLocal 的内部实现依赖于一个称为 ThreadLocalMap 的数据结构。以下是其主要实现细节:

ThreadLocal类:ThreadLocal类本身非常简单,主要包含三个方法:get()set(T value)remove()。它利用 ThreadLocalMap 来存储和管理线程局部变量。

ThreadLocalMap:每个线程(Thread对象)内部都有一个 ThreadLocalMap 实例,该实例用来存储 ThreadLocal 变量及其对应的值。ThreadLocalMapThreadLocal 类的静态内部类,它使用弱引用(WeakReference)来引用 ThreadLocal 对象,从而避免内存泄漏。

ThreadLocal 的主要方法

initialValue 方法:默认返回 null。用户可以通过重写此方法为 ThreadLocal 变量提供一个初始值。

1
2
3
protected T initialValue() {
return null;
}

withInitial 方法: 用于创建一个 ThreadLocal 实例,并通过 Supplier 提供初始值。这种方式比重写 initialValue 方法更加简洁和易于使用。

1
2
3
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}

get 方法:返回当前线程中此 ThreadLocal 变量的值。如果该线程是第一次调用此方法,则调用 initialValue() 方法并将结果存储在该线程的 ThreadLocalMap 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从ThreadLocalMap中获取当前ThreadLocal变量的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果ThreadLocalMap为空或者没有找到对应的值,调用setInitialValue()方法
return setInitialValue();
}

// 其中的 getMap 方法主要是通过 TreadLocal 获取 ThreadLocal 对象:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

setInitialValue 方法: 用于调用 initialValue() 方法获取初始值,并将该值存储在当前线程的 ThreadLocalMap 中。

1
2
3
4
5
6
7
8
9
10
11
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
return value;
}

set 方法:将当前线程中此 ThreadLocal 变量的值设置为指定值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 将指定值存储在ThreadLocalMap中
map.set(this, value);
} else {
// 如果ThreadLocalMap为空,则创建一个新的ThreadLocalMap并存储指定值
createMap(t, value);
}
}

// 其中的 createMap 方法,作用在上述注释里
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

remove 方法:移除当前线程中此 ThreadLocal 变量的值,有助于防止内存泄漏。

1
2
3
4
5
6
7
8
9
10
public void remove() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从ThreadLocalMap中移除当前ThreadLocal变量的值
map.remove(this);
}
}
上述代码展示了 ThreadLocal 及其内部 ThreadLocalMap 类的关键方法的实现细节。这些方法共同确保了每个线程可以拥有独立的 ThreadLocal 变量副本(ThreadLocalMap实例),从而实现线程局部变量的功能(可以使用 InheritableThreadLocal 来实现多个线程访问 ThreadLocal 的值)。

ThreadLocal 优点

线程隔离

ThreadLocal 允许每个线程拥有自己的数据副本,这些副本互不干扰。每个线程只能访问和修改它自己存储的数据,而不会影响其他线程。这种隔离避免了多线程环境中共享数据带来的线程安全问题。

简化线程安全编程

由于每个线程都有独立的数据副本,不需要使用同步机制来保护数据的访问和修改,这使得编写线程安全的代码变得更简单。你不需要显式地处理同步问题,ThreadLocal 会为每个线程自动管理数据。

提高性能

使用 ThreadLocal 可以减少由于加锁造成的性能开销,因为每个线程都有独立的数据副本,不需要进行线程间的锁竞争。这通常会比使用同步数据结构(如ConcurrentHashMap)具有更高的性能,尤其是在高并发的场景中。

简化编程模型

在需要在多线程环境中共享数据时,ThreadLocal 提供了一种简单的方式来存储和访问线程相关的数据。它减少了显式的同步操作和复杂的线程间通信,使得程序逻辑更加直观和简洁。

避免全局状态

ThreadLocal 可以用来避免使用全局状态或单例模式来存储线程相关的数据。全局状态可能会导致线程安全问题,而 ThreadLocal 能够保证数据的隔离性和独立性,从而提高了系统的可靠性。

简单使用 ThreadLocal

创建和使用ThreadLocal

使用 ThreadLocal 非常简单,以下是一些基本步骤:

定义 ThreadLocal 变量
通常使用静态变量,以确保所有线程共享同一个 ThreadLocal 实例。

1
2
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = 
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

获取 ThreadLocal 变量的值
通过 get() 方法获取当前线程中 ThreadLocal 变量的值。

1
SimpleDateFormat dateFormat = dateFormatThreadLocal.get();

设置 ThreadLocal 变量的值

1
dateFormatThreadLocal.set(new SimpleDateFormat("MM-dd-yyyy"));

移除 ThreadLocal 变量的值
使用 remove() 方法移除当前线程中 ThreadLocal 变量的值。

1
dateFormatThreadLocal.remove();

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadLocalExample {
private static final ThreadLocal<Integer> TEST_THREAD_LOCAL = ThreadLocal.withInitial(() -> 1);

public static void main(String[] args) {
Runnable task = () -> {
int value = TEST_THREAD_LOCAL.get();
System.out.println(Thread.currentThread().getName() + ": Initial Value = " + value);
TEST_THREAD_LOCAL.set(++value);
System.out.println(Thread.currentThread().getName() + ": Modified Value = " + TEST_THREAD_LOCAL.get());
};

// 本案例为示例代码,实际不建议显式创建线程,建议线程池!!
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);

thread1.start();
thread2.start();
}
}

应用场景之一:存储用户信息

创建一个 ThreadLocal 存储类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author -侑枫
* @date 2024/7/21 2:38:26
*/
@UtilityClass
public class CurrentUserContext {

private static final ThreadLocal<UserDTO> CURRENT_USER = new ThreadLocal<>();

public static void setCurrentUser(UserDTO userDTO) {
CURRENT_USER.set(userDTO);
}

public static UserDTO getCurrentUser() {
return CURRENT_USER.get();
}

public static void clean() {
CURRENT_USER.remove();
}
}
@UtilityClassLombok 的一个注解,作用是将其标记为工具类,使其成为一个静态类,不能再创建实例。(原理也很简单,私有化掉构造器而已)

创建一个过滤器来拦截请求并设置用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @author -侑枫
* @date 2024/7/21 2:43:47
*/
public class UserFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

try {
String loginName = SecurityUtils.getLoginName();
if (ObjectUtils.isNotEmpty(loginName)) {
// 此处建议可以先从 redis 缓存中获取,没有再去查数据库,然后将结果存入缓存中
UserDTO userDTO = UserUtils.getByLoginName(loginName);
if (ObjectUtils.isNotEmpty(userDTO)) {
CurrentUserContext.setCurrentUser(userDTO);
}
}
chain.doFilter(request, response);
} finally {
CurrentUserContext.clean();
}
}

@Override
public void destroy() {
Filter.super.destroy();
}
}

注册过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author -侑枫
* @date 2024/7/21 2:58:03
*/
@Configuration
public class MyFilterConfig {

@Bean
public FilterRegistrationBean<UserFilter> userFilter() {
FilterRegistrationBean<UserFilter> registration = new FilterRegistrationBean<>();

registration.setFilter(new UserFilter());
// 这里我犯了一个错误 不应该包含 context-path 例如 /api
registration.addUrlPatterns("/*");

return registration;
}
}

addUrlPatterns 源码:

1
2
3
4
5
6
7
8
9
/**
* Add URL patterns, as defined in the Servlet specification, that the filter will be
* registered against.
* @param urlPatterns the URL patterns
*/
public void addUrlPatterns(String... urlPatterns) {
Assert.notNull(urlPatterns, "UrlPatterns must not be null");
Collections.addAll(this.urlPatterns, urlPatterns);
}

使用 UserContext 获取用户信息

1
2
3
4
5
6
7
8
// UserController 部分代码
/**
* 获取当前用户
*/
@GetMapping
public UserDTO getUser() {
return CurrentUserContext.getCurrentUser();
}

测试

接下来 我们同时在不同浏览器登陆不同的账号,再使用 PostMan 分别调用这个请求

管理员x

管理员

管理员

管理员x

通过 CurrentUserContext类,我们能够在每个线程中独立地存储和访问当前用户的信息,从而避免了多线程环境中的数据共享问题。UserFilter 类则展示了如何在请求过滤器中利用 ThreadLocal 来设置和清理当前用户信息,确保每个请求都能正确地获取用户信息,且在使用后需要及时清理,防止内存泄漏。

内存泄漏及预防

在使用 ThreadLocal 时,一个常见的问题是内存泄漏。这种情况通常发生在使用线程池的环境中,因为线程池中的线程会被重复使用,这可能导致 ThreadLocal 中的引用无法被垃圾回收。了解内存泄漏的成因及其预防措施对于确保应用的稳定性和性能至关重要。

内存泄漏的成因

线程池中的线程复用

当使用 ThreadLocal 存储数据时,每个线程都有一个独立的 ThreadLocalMap 实例来保存数据。如果线程池中的线程在处理多个任务时没有正确清理 ThreadLocal 中的数据,旧的数据可能仍然被保留在内存中,从而导致内存泄漏。具体来说,当线程池中的线程被复用时,旧数据会残留在 ThreadLocal 中,导致这些数据无法被垃圾回收

方法

ThreadLocal 中的值在不再需要时应该被显式地移除。如果开发者忽略了调用 remove 方法来清理 ThreadLocal 中的数据,这些数据可能会一直占用内存,尤其是在长期运行的应用中

在实际应用场景中,假设上一个请求中,用户 A 存储了他的信息在 ThreadLocal 中,但退出登录后,没有调用 remove 来进行清理,那么未登录用户,可能获取到上一个用户的信息(据传手游某某荣耀之前就出现过这样的问题)

预防措施

在任务完成后清理数据: 在使用 ThreadLocal 存储线程本地数据时,确保在任务完成后调用 remove 方法清理数据。这样可以避免线程池中的线程被复用时旧的数据仍然存在

使用 try-finally 结构: 在处理每个请求或任务时,使用 try-finally 结构确保 ThreadLocal 中的数据始终被清理。这样即使在处理过程中发生异常,finally 代码块中的清理操作也会被执行

考虑使用 withInitial 方法: 如果你的 ThreadLocal 实例使用的是 Java 8 及以上版本,可以使用 ThreadLocal.withInitial 方法,它提供了一种简单的方式来初始化 ThreadLocal,并且不会引入额外的内存泄漏风险

示例:ThreadLocal 的使用与清理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UserFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
// 在请求处理之前设置线程本地数据
String loginName = SecurityUtils.getLoginName();
if (ObjectUtils.isNotEmpty(loginName)) {
UserDTO userDTO = UserUtils.getByLoginName(loginName);
if (ObjectUtils.isNotEmpty(userDTO)) {
CurrentUserContext.setCurrentUser(userDTO);
}
}

// 继续处理请求
chain.doFilter(request, response);
} finally {
// 在请求处理完成后清理线程本地数据
CurrentUserContext.clean();
}
}
}

结语

Java 中,ThreadLocal 是一种强大而灵活的工具,它允许每个线程拥有独立的数据副本,从而简化多线程编程中的数据管理问题。通过理解 ThreadLocal 的优势和使用注意事项,我们能够更有效地利用其功能,简化多线程编程的复杂性,并确保应用程序的性能和稳定性。在实际开发中,遵循最佳实践并仔细管理线程本地数据,将帮助你构建高效且可靠的多线程应用程序。

  • 标题: ThreadLocal 介绍与简单使用
  • 作者: HYF
  • 创建于 : 2024-07-21 02:12:56
  • 更新于 : 2024-07-27 21:21:50
  • 链接: https://youfeng.ink/ThreadLocal-InfoStorage-331e234afbf6/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。