ThreadLocal 介绍与简单使用
线程封闭是一种通过限制数据的访问范围来实现线程安全的技术。其核心思想是将数据限制在单个线程的上下文中,使得其他线程无法直接访问或修改这些数据。这种方法避免了线程间的数据竞争和同步问题。ThreadLocal
是线程封闭的一种实现。
什么是 ThreadLocal
ThreadLocal
是 Java
提供的一种线程局部变量,用于在多线程环境中为每个线程提供独立的变量副本。它的工作机制可以简单描述为:每个线程都拥有一个独立的 ThreadLocal
变量副本:当一个线程访问 ThreadLocal
变量时,它实际上访问的是该线程自己持有的副本,而不是其他线程共享的变量。这种机制确保了线程间的数据隔离和线程安全。
ThreadLocal 实现细节
ThreadLocal
的内部实现依赖于一个称为 ThreadLocalMap
的数据结构。以下是其主要实现细节:
ThreadLocal类:ThreadLocal类本身非常简单,主要包含三个方法:get()
、set(T value)
和 remove()
。它利用 ThreadLocalMap
来存储和管理线程局部变量。
ThreadLocalMap:每个线程(Thread
对象)内部都有一个 ThreadLocalMap
实例,该实例用来存储 ThreadLocal
变量及其对应的值。ThreadLocalMap
是 ThreadLocal
类的静态内部类,它使用弱引用(WeakReference)
来引用 ThreadLocal
对象,从而避免内存泄漏。
ThreadLocal 的主要方法
initialValue 方法:默认返回 null
。用户可以通过重写此方法为 ThreadLocal
变量提供一个初始值。
1 | protected T initialValue() { |
withInitial 方法: 用于创建一个 ThreadLocal
实例,并通过 Supplier
提供初始值。这种方式比重写 initialValue
方法更加简洁和易于使用。
1 | public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { |
get 方法:返回当前线程中此 ThreadLocal
变量的值。如果该线程是第一次调用此方法,则调用 initialValue()
方法并将结果存储在该线程的 ThreadLocalMap
中。
1 | public T get() { |
setInitialValue 方法: 用于调用 initialValue()
方法获取初始值,并将该值存储在当前线程的 ThreadLocalMap
中。
1 | private T setInitialValue() { |
set 方法:将当前线程中此 ThreadLocal
变量的值设置为指定值。
1 | public void set(T value) { |
remove 方法:移除当前线程中此 ThreadLocal
变量的值,有助于防止内存泄漏。
1 | public void remove() { |
上述代码展示了 ThreadLocal
及其内部 ThreadLocalMap
类的关键方法的实现细节。这些方法共同确保了每个线程可以拥有独立的 ThreadLocal
变量副本(ThreadLocalMap
实例),从而实现线程局部变量的功能(可以使用 InheritableThreadLocal
来实现多个线程访问 ThreadLocal
的值)。
ThreadLocal 优点
线程隔离
ThreadLocal
允许每个线程拥有自己的数据副本,这些副本互不干扰。每个线程只能访问和修改它自己存储的数据,而不会影响其他线程。这种隔离避免了多线程环境中共享数据带来的线程安全问题。
简化线程安全编程
由于每个线程都有独立的数据副本,不需要使用同步机制来保护数据的访问和修改,这使得编写线程安全的代码变得更简单。你不需要显式地处理同步问题,ThreadLocal
会为每个线程自动管理数据。
提高性能
使用 ThreadLocal
可以减少由于加锁造成的性能开销,因为每个线程都有独立的数据副本,不需要进行线程间的锁竞争。这通常会比使用同步数据结构(如ConcurrentHashMap
)具有更高的性能,尤其是在高并发的场景中。
简化编程模型
在需要在多线程环境中共享数据时,ThreadLocal
提供了一种简单的方式来存储和访问线程相关的数据。它减少了显式的同步操作和复杂的线程间通信,使得程序逻辑更加直观和简洁。
避免全局状态
ThreadLocal
可以用来避免使用全局状态或单例模式来存储线程相关的数据。全局状态可能会导致线程安全问题,而 ThreadLocal
能够保证数据的隔离性和独立性,从而提高了系统的可靠性。
简单使用 ThreadLocal
创建和使用ThreadLocal
使用 ThreadLocal
非常简单,以下是一些基本步骤:
定义 ThreadLocal 变量
通常使用静态变量,以确保所有线程共享同一个 ThreadLocal
实例。
1 | private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = |
获取 ThreadLocal 变量的值
通过 get()
方法获取当前线程中 ThreadLocal
变量的值。
1 | SimpleDateFormat dateFormat = dateFormatThreadLocal.get(); |
设置 ThreadLocal 变量的值
1 | dateFormatThreadLocal.set(new SimpleDateFormat("MM-dd-yyyy")); |
移除 ThreadLocal 变量的值
使用 remove()
方法移除当前线程中 ThreadLocal
变量的值。
1 | dateFormatThreadLocal.remove(); |
简单示例
1 | public class ThreadLocalExample { |
应用场景之一:存储用户信息
创建一个 ThreadLocal 存储类
1 | /** |
@UtilityClass
是 Lombok
的一个注解,作用是将其标记为工具类,使其成为一个静态类,不能再创建实例。(原理也很简单,私有化掉构造器而已)
创建一个过滤器来拦截请求并设置用户信息
1 | /** |
注册过滤器
1 | /** |
addUrlPatterns
源码:
1 | /** |
使用 UserContext 获取用户信息
1 | // UserController 部分代码 |
测试
接下来 我们同时在不同浏览器登陆不同的账号,再使用 PostMan
分别调用这个请求
通过 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 | public class UserFilter implements Filter { |
结语
在 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 进行许可。