需求背景
假设我们正在开发一个基于微服务架构的在线服务平台,该平台提供了用户认证、数据存取、业务处理等功能。在这个平台中,用户通过登录流程获取一个认证Token,该Token用于在后续的请求中验证用户身份。为了保证Token的安全性和隔离性,我们需要一种机制来确保每个用户请求对应的Token只在处理该请求的线程中有效,并且不会被其他线程访问或篡改。
为了解决这个问题,我们可以利用ThreadLocal
的特性来创建一个线程封闭的存储空间,专门用于保存每个用户的Token。这篇文章就是介绍一下ThreadLocal
的一些知识
解决方法
ThreadLocal
是 Java 中用于线程封闭的类,它允许线程创建私有变量副本,确保线程间互不影响。这一特性对于实现线程安全极为有效,因为每个线程只能访问自己的变量。
ThreadLocal
的常见用途包括:
- 存储线程相关的数据(例如,在一个 Web 应用程序中存储用户的 ID 或事务 ID)。
- 避免线程间共享资源的开销,例如使用 SimpleDateFormat 处理日期。
当我们在代码中声明一个 ThreadLocal
变量时,实际上它的值被保存在当前线程的 ThreadLocalMap
中。ThreadLocalMap
是 ThreadLocal
的一个内部私有类,可以看作是一个以 ThreadLocal
对象的弱引用为键、以实际存储的用户值为值的映射。
关于ThreadLocalMap
ThreadLocalMap
使用了弱引用作为键(key),,值(value)强引用。这意味着一旦外部没有强引用指向 ThreadLocal
实例,就可能会被垃圾回收器回收。在这种情况下,对应的键值会变成 null。如果 ThreadLocalMap
的条目的键是 null,那么这个条目也就无法访问到了,而与之关联的值则可能继续占用内存,这会导致内存泄漏。
为什么使用弱引用?
在JDK中,ThreadLocalMap使用弱引用作为键的原因是为了解决内存泄漏的问题,特别是在ThreadLocal对象不再被使用时。弱引用提供了一种机制,当ThreadLocal对象不再有其他强引用时,它能够被垃圾回收器回收,即使它仍然被存储在Thread对象的ThreadLocalMap中。
怎么解决内存泄漏的问题?
- 手动清除 :最直接的方法是不再使用时调用
ThreadLocal
的remove()
方法来删除对应线程的值。这个方法会从ThreadLocalMap
中清除当前线程的值。
java
threadLocal.remove();
这个方法可以让你知道 ThreadLocal
不再需要时调用,比如在 HttpServletRequest 的处理过程结束后。
- 使用完毕尽快清理 :在使用
ThreadLocal
变量进行线程间操作完毕之后,尽可能地在代码的 finally 块中调用remove()
方法,避免因为异常导致的没有清理ThreadLocal
变量。 - JVM 自身的清理机制 :虽然 Java 虚拟机会在键对象被回收时将键设置为 null,但
ThreadLocalMap
也实现了自己的一套防泄漏机制。ThreadLocalMap
的get()
、set()
和remove()
方法都会清理已经变为 null 的键的条目。这种清除工作是在正常的ThreadLocal
操作过程中顺带完成的。
然而,如果一个线程生命周期很长,并且不再访问 ThreadLocal
,这种清理机制可能无法运作,因为没有进一步的 ThreadLocal
方法调用来触发清理过程,这可能会导致内存泄漏。因此,手动调用 remove()
方法总是一种更安全的做法。
下面是一个 Java 示例代码,演示了如何使用两个 ThreadLocal
变量,以及如何在使用它们时清理资源以防止内存泄漏:
java
public class ThreadLocalExample {
// 创建两个 thread-local 变量
private static final ThreadLocal<String threadLocalVar1 = new ThreadLocal<>();
private static final ThreadLocal<Integer threadLocalVar2 = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 模拟多个线程使用 thread-local 变量
for (int i = 0; i < 3; i++) {
int finalI = i;
new Thread(() -> {
try {
// 每个线程设置 thread-local 变量
threadLocalVar1.set("Thread-" + finalI + " data for var1");
threadLocalVar2.set(finalI);
// 使用 thread-local 变量
System.out.println(Thread.currentThread().getName()
+ " value: " + threadLocalVar1.get()
+ " / " + threadLocalVar2.get());
} finally {
// 必须在最后清理 thread-local 变量以避免内存泄漏
threadLocalVar1.remove();
threadLocalVar2.remove();
}
}).start();
}
}
}
在这个示例中,我们定义了两个 ThreadLocal
变量:threadLocalVar1
和 threadLocalVar2
。每个线程都会设置它们的值,然后输出这些值,以证明每个线程都有自己的独立拷贝。
请注意在 finally
代码块中,我们调用了 remove()
方法。这是非常重要的一步,因为它确保了不再使用的 ThreadLocal
变量能够被垃圾收集器清理,从而防止了潜在的内存泄漏。
每个线程结束后,在 finally
块中调用 remove()
确保 ThreadLocal
存储的值被移除,这样即便 ThreadLocal
的实例被回收,ThreadLocalMap
中也不会留下无用的值。
日期格式化工具 SimpleDateFormat的实践
SimpleDateFormat
类在 Java 中用于日期的解析与格式化,但它不是线程安全的。意味着在多线程环境下直接使用同一个 SimpleDateFormat
实例时,可能会产生并发问题,比如解析错误或者不正确的日期格式。为了避免这个问题,传统的方法是在每次需要的时候创建一个新的 SimpleDateFormat
实例,但这样做在高并发的场景下会造成性能问题,因为对象的创建和销毁是有成本的。
使用 ThreadLocal<SimpleDateFormat>
可以为每个线程创建一个单独的 SimpleDateFormat
实例,这样每个线程都可以安全地使用这个实例而不用担心并发问题。这样,不仅避免了并发问题,还通过减少对象创建和垃圾回收的次数来提高了性能。
下面是如何使用 ThreadLocal<SimpleDateFormat>
的代码示例:
java
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateFormatExample {
// 使用 ThreadLocal 为每个线程创建一个 SimpleDateFormat 实例
private static final ThreadLocal<SimpleDateFormat dateFormatThreadLocal =
new ThreadLocal<SimpleDateFormat() {
@Override
protected SimpleDateFormat initialValue() {
// 这个方法将为每个访问这个 ThreadLocal 的线程返回一个新的 SimpleDateFormat 实例
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
// 方法,返回当前线程的日期格式化
public static String formatDate(Date date) {
return dateFormatThreadLocal.get().format(date);
}
// 线程调用该方法
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
String dateStr = formatDate(new Date());
System.out.println(Thread.currentThread().getName() + " formatted date: " + dateStr);
}).start();
}
}
}
在这个例子中,我们使用 ThreadLocal
创建了一个 SimpleDateFormat
的线程局部变量,initialValue()
方法保证了每个线程调用 get()
方法时,如果没有自己的 SimpleDateFormat
,就会创建一个新的实例。所有的线程都会使用 formatDate()
方法来格式化日期,这会使用到 ThreadLocal
存储的 SimpleDateFormat
实例,确保了线程安全并且减少了对象创建的开销。eadLocalMap对象变得无法访问,从而使内部的
SimpleDateFormat` 实例成为垃圾回收的目标。