ThreadLocal

ThreadLocal

ThreadLocal 是 Java 中的一个类,用于创建线程局部变量。每个线程都有自己的变量副本,互不干扰ThreadLocal 可以用于在多线程环境下保持线程间独立的数据,常见的使用场景包括:

  1. 线程安全的数据共享: ThreadLocal 可以用于在多线程环境下安全地共享数据,每个线程拥有自己的数据副本,互不影响。
  2. 上下文传递: 可以使用 ThreadLocal 传递上下文信息,避免显式传递参数的麻烦。例如,在Web应用中,可以将用户身份信息、请求信息等存储在 ThreadLocal 中,方便在各个层次的代码中访问。
  3. 避免传递参数的复杂性: 在某些情况下,将参数传递给每个方法都会显得繁琐,使用 ThreadLocal 可以避免这种复杂性,因为数据被存储在线程本地。
  4. 线程池场景: 在使用线程池时,可以使用 ThreadLocal 存储一些线程私有的状态,而不需要担心线程复用时数据混乱。
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
public class Main {
private static ThreadLocal<Integer> threadLocalValue1 = new ThreadLocal<Integer>();
private static ThreadLocal<Integer> threadLocalValue2 = new ThreadLocal<Integer>();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
threadLocalValue1.set(100);
System.out.println("Thread 1: " + threadLocalValue1.get());
threadLocalValue1.remove();
threadLocalValue2.set(101);
System.out.println("Thread 1: " + threadLocalValue2.get());
threadLocalValue2.remove();

});

Thread t2 = new Thread(() -> {
threadLocalValue1.set(200);
System.out.println("Thread 2: " + threadLocalValue1.get());
threadLocalValue1.remove();
threadLocalValue2.set(201);
System.out.println("Thread 1: " + threadLocalValue2.get());
threadLocalValue2.remove();
});

t1.start();
t2.start();
}

}

Thread类源代码入手

1
2
3
4
5
6
7
8
9
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法

ThreadLocal的数据结构

image-20240305220202288

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

1
2
3
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//......
}

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话, Thread内部都是使用仅有的那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal<?>value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离

ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,ThreadLocalMap中并没有链表结构。我们还要注意Entry, 它的keyThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。

总结:

  • 每个Thread线程内部都有一个ThreadLocalMap。
  • Map里面存储线程本地对象ThreadLocal(key)和线程的变量副本(value)。
  • Thread内部的Map是由ThreadLocal维护,ThreadLocal负责向map获取和设置线程的变量值。
  • 一个Thread可以有多个ThreadLocal。

ThreadLocal 内存泄露问题是怎么导致的?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

弱引用介绍:

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

为了避免 ThreadLocal 内存泄露问题,可以采取以下一些建议:

  • 及时清理: 在使用完 ThreadLocal 后,及时调用 remove 方法清理。可以使用 try-with-resources 或者 finally 块确保在线程结束时调用 remove

为什么ThreadLocal的key是弱引用?

线程在我们应用中,常常是以线程池的方式来使用的,比如 Tomcat 的线程池处理了一堆请求,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着。所以这条引用链需要弱化一下,而能操作的只有 Entry 和 key 之间的引用,所以它们之间用弱引用来实现。

image-20240903223631641

另一条引用链就是栈上的 ThreadLocal 引用指向堆中的 ThreadLocal 对象,这个引用是强引用。如果有这条强引用存在,那说明此时的 ThreadLocal 是有用的,此时如果发生 GC 则 ThreadLocal 对象不会被清除,因为有个强引用存在。

当随着方法的执行完毕,相应的栈帧也出栈了,此时这条强引用链就没了,如果没有别的栈有对 ThreadLocal 对象的引用,那么说明 ThreadLocal 对象无法再被访问到(定义成静态变量的另说)

那此时 ThreadLocal 只存在与 Entry 之间的弱引用,那此时发生 GC 它就可以被清除了,因为它无法被外部使用了,那就等于没用了,是个垃圾,应该被处理来节省空间。

至此,想必你已经明白为什么 Entny 和 key之间要设计为弱引用,就是因为平日线程的使用方式基本上都是线程池,所以线程的生命周期就很长,可能从你部署上线后一直存在,而 ThreadLocal 对象的生命周期可能没这么长。

所以为了能让已经没用 ThreadLocal对象得以回收,所以 Entry 和 key 要设计成弱引用,不然 Entry 和 key是强引用的话ThreadLocal 对象就会一直在内存中存在。但是这样设计就可能产生内存泄漏。

ThreadLocal的应用场景

  1. 用户会话信息存储:在 Web 应用中,每个用户的会话信息(如用户ID、权限、购物车等)可以通过 ThreadLocal 存储,确保每个线程在后续的请求都能访问正确并快速的访问用户会话信息
  2. 数据库连接:在多线程环境中,每个线程可以独立管理自己的数据库连接,通过 ThreadLocal 存储,这样可以避免数据库连接的频繁创建和销毁,提高性能。比如MyBatis中的SqlSession对象就使用了ThreadLocal来存储当前线程的数据库会话信息
  3. 事务管理:在分布式系统中,事务的上下文(如事务ID、当前状态等)可以通过 ThreadLocal 存储,每个线程独立的控制自己的事务,保证事务的隔离性。Spring中的TransactionSynchronizationManager类就是用ThreadLocal来存储事务相关的上下文信息。
  4. 日志记录:在多线程应用中,可以通过 ThreadLocal 存储日志记录器(Logger)的实例,确保每个线程的日志记录不会相互干扰。

ThreadLocal
http://example.com/2023/05/13/Java/Java多并发/ThreadLocal/
作者
PALE13
发布于
2023年5月13日
许可协议