深入浅出 JAVA 之线程 - ThreadLocal 详解
文章目录
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。
系统环境:
- JDK 版本: OpenJDK 11
一、ThreadLocal 简介
ThreadLocal 是一个用于实现线程本地存储的工具类,可以让线程在使用某些对象时保证其线程安全。在多线程场景下,线程可以共享一些变量或者对象,但是如果这些共享资源不是线程安全的,在多线程环境下就会出现一些问题。为了解决这些问题,可以使用 ThreadLocal 将这些变量或对象进行隔离,使每个线程都能够独立的使用自己的变量或对象,达到线程安全的效果。
通常 ThreadLocal 应该定义为 static
类型,以保证在所有线程中只有一份,而通过 set()
和 get()
方法访问同一个 ThreadLocal 变量时,每个线程都有自己的一个变量副本,互不干扰。
在线程池中使用 ThreadLocal 时,需要在任务执行完成后手动调用 remove()
方法清除 ThreadLocal 中的数据,以避免数据的泄露。
二、ThreadLocal 中的方法
在 ThreadLocal 中提供了以下几个主要的方法:
① 方法 initialValue()
用于初始化变量的方法,可以通过覆盖重写此方法来定义初始化逻辑。默认情况下 initialValue() 方法返回 null。
② 方法 get()
获取当前线程的 ThreadLocal 变量副本的值。如果当前线程尚未设置 ThreadLocal 变量副本,则会返回初始值或 null (这里的初始值由 initialValue() 方法设置,并且默认为初始值为 null)。
获取当前线程的变量副本。如果当前线程没有设置过该变量,则会调用 initialValue() 方法进行初始化,并返回初始化后的值。
③ 方法 set(value)
设置当前线程的 ThreadLocal 变量副本的值为指定的值。如果当前线程尚未设置 ThreadLocal 变量副本,则会创建一个新的副本并将指定的值设置为其初始值。如果当前线程已经有一个 ThreadLocal 变量副本,则会更新其值为指定的值。
④ 方法 remove()
移除当前线程的 ThreadLocal 变量副本,以防止内存泄漏。
⑤ 方法 withInitial(Supplier<? extends S> supplier)
创建一个 ThreadLocal 对象,并通过提供的 Supplier 函数式接口来初始化该对象的值。
三、ThreadLocal 使用示例
3.1 ThreadLocal 基本操作示例
下面是一个 ThreadLocal 的基本操作示例:
1public class ThreadLocalDemo {
2
3 private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
4 // 重写 initialValue 方法,初始化线程本地变量的值
5 @Override
6 protected Integer initialValue() {
7 return 0;
8 }
9 };
10
11 /**
12 * 线程类
13 */
14 static class MyThread implements Runnable {
15 @Override
16 public void run() {
17 for (int i = 0; i < 3; i++) {
18 try {
19 // 获取线程本地变量的值
20 int num = threadLocal.get();
21 // 操作线程本地变量的值
22 num += 1;
23 // 设置线程本地变量的值
24 threadLocal.set(num);
25 // 输出线程本地变量的值
26 System.out.println(Thread.currentThread().getName() + ": " + num);
27 // 线程暂停,模拟业务操作
28 Thread.sleep(200);
29 } catch (InterruptedException e) {
30 e.printStackTrace();
31 Thread.currentThread().interrupt();
32 }
33 }
34 // 最后调用 remove 方法进行清理
35 threadLocal.remove();
36 }
37 }
38
39 public static void main(String[] args) {
40 MyThread myThread = new MyThread();
41 Thread thread1 = new Thread(myThread, "Thread1");
42 Thread thread2 = new Thread(myThread, "Thread2");
43 thread1.start();
44 thread2.start();
45 }
46
47}
然后运行上面代码,将会输入如下结果:
1Thread2: 1
2Thread1: 1
3Thread1: 2
4Thread2: 2
5Thread1: 3
6Thread2: 3
在上述示例中,我们首先定义了一个 ThreadLocal 对象,用于存储线程本地变量。通过重写 ThreadLocal 的 initialValue()
方法,我们可以初始化线程本地变量的值,并将其初始值设为 0。
然后,我们创建了一个名为 MyThread 的线程类,实现了 Runnable 接口。在 run()
方法中,我们通过 threadLocal.get()
方法获取线程本地变量的值,并对其进行操作。然后,通过 threadLocal.set()
方法来设置线程本地变量的值。在每次操作后,我们输出当前线程的名称和线程本地变量的值。为了模拟业务操作的延迟,我们使用 Thread.sleep()
方法来让线程缓慢执行。
最后,在主函数中,我们创建了两个名为 Thread1 和 Thread2 的线程,并分别启动它们。每个线程都有自己的线程本地变量,它们之间是相互独立的,互不干扰。在线程执行结束后,我们通过 threadLocal.remove()
方法来删除线程本地变量,释放资源。
通过这个示例,我们可以清楚地了解 ThreadLocal 的使用方式,并且展示了如何避免线程之间的竞争问题。每个线程都可以独立地访问和修改自己的线程本地变量,而不会对其他线程产生影响。这使得我们可以在多线程环境下安全地处理线程特定的数据,并且不需要担心线程安全性问题。
3.2 ThreadLocal 包装 SimpleDateFormat 示例
我们知道 SimpleDateFormat 不是线程安全的,如果多个线程共享同一个 SimpleDateFormat 实例,在并发访问时可能会导致日期解析和格式化的错误结果。而通过使用 ThreadLocal,每个线程都可以拥有自己的 SimpleDateFormat 实例,可以确保线程安全性。
下面是一个使用 ThreadLocal 包装 SimpleDateFormat 的示例代码:
1import java.text.SimpleDateFormat;
2import java.util.Date;
3
4public class DateFormatThreadLocal {
5
6 /**
7 * 使用 ThreadLocal 包装 SimpleDateFormat
8 */
9 private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<SimpleDateFormat>() {
10 @Override
11 protected SimpleDateFormat initialValue() {
12 // 创建 SimpleDateFormat 对象,并指定日期格式
13 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
14 }
15 };
16
17 /**
18 * 获取 SimpleDateFormat 实例
19 */
20 public static SimpleDateFormat getDateFormat() {
21 return DATE_FORMAT_THREAD_LOCAL.get();
22 }
23
24 public static void main(String[] args) {
25 String date = getDateFormat().format(new Date());
26 System.out.println(date);
27 }
28
29}
在上面的代码中,我们使用一个 ThreadLocal 包装了 SimpleDateFormat 对象,使用 initialValue()
方法创建一个 SimpleDateFormat 实例,并指定日期格式。
然后通过 getDateFormat()
方法获取 SimpleDateFormat 实例,在使用过程中,每个线程都会获取到一个独立的 SimpleDateFormat 实例,并且互不干扰,避免了多线程并发访问时的线程安全问题。
使用 ThtreadLocal 包装 SimpleDateFormat 的这种方式的优势在于,它既解决了 SimpleDateFormat 的线程安全问题,又减少了对象的创建和销毁,从而提高了性能。
四、ThreadLocal 使用场景
ThreadLocal 在多线程编程中有很多使用场景,主要用于解决线程安全性和上下文隔离的问题。以下是一些常见的 ThreadLocal 使用场景:
- 线程安全的对象实例: 当一个对象不是线程安全的,并且需要在多个线程中使用时,可以使用 ThreadLocal 来为每个线程提供独立的对象实例,确保线程安全。
- 用户身份信息: 在 Web 应用程序中,常常需要保存用户的身份信息,如登录用户的ID、用户名等。使用 ThreadLocal 可以在每个线程中存储用户的身份信息,方便在整个请求处理过程中访问和验证用户身份,而无需传递参数或使用全局变量。
- 数据库连接管理: 在多线程环境中使用数据库连接时,可以将数据库连接保存在 ThreadLocal 中,以确保每个线程都有独立的数据库连接,避免线程间的竞争和资源泄露。
- 事务管理: 在需要进行事务管理的应用中,可以使用 ThreadLocal 来存储事务上下文信息,确保每个线程独立地管理自己的事务,而不会干扰其他线程的事务操作。
- 国际化(i18n): 在国际化应用中,可以使用 ThreadLocal 来存储当前线程的语言环境 (Locale),以便在应用的不同部分动态地获取当前线程的语言环境,从而实现多语言支持。
- 缓存管理: ThreadLocal 也可用于缓存管理,将缓存对象保存在 ThreadLocal 中,每个线程都可以独立地访问和管理自己的缓存,避免了线程间的冲突和同步开销。
总结来说 ThreadLocal 适用于需要在线程之间隔离数据或上下文的场景,特别是在多线程环境下需要保证线程安全性的情况下,它提供了一种简单且有效的方式来实现线程局部变量的管理。
注意: ThreadLocal 虽然在解决线程安全性问题上非常有用,但也需要慎重使用。过度使用 ThreadLocal 可能会导致代码的可读性和维护性变差,导致线程之间的依赖关系不够明确。此外,如果 ThreadLocal 变量被大量使用且不及时清理,可能会导致内存泄漏的问题。因此,在使用 ThreadLocal 时应注意适度使用,并及时清除 ThreadLocal 变量副本。
五、ThreadLocal 实现原理
下图是与 ThreadLocal 的实现相关的组件结构,可以先参照该结构图理,集合下面实现原理中的步骤,理解 ThreadLocal 是如何实现的。
下面是 ThreadLocal 的简要实现原理,如下:
- ① 每个线程 Thread 对象内部都有一个 ThreadLocalMap 类型的变量 threadLocals,用于存储线程本地变量。而 ThreadLocalMap 是 ThreadLocal 中的一个内部类,它使用了自定义的哈希表结构。
- ② ThreadLocalMap 是一个自定义的哈希表结构,它会使用
Entry[]
存储了多个键值对Entry
对象,其中Entry.key
是 ThreadLocal 实例对象,Entry.value
是线程对应的变量副本。每个线程都拥有自己的 ThreadLocalMap 对象。 - ③ 当通过 ThreadLocal 的
set()
方法设置线程本地变量时,首先获取当前线程的 ThreadLocalMap 对象,如果不存在则创建一个新的 ThreadLocalMap 并设置到当前线程。然后创建键值对对象Entry
,并且将 ThreadLocal 实例对象作为Entry.key
,将变量副本作为Entry.value
存储到 ThreadLocalMap 中。 - ④ 当通过 ThreadLocal 的
get()
方法获取线程本地变量时,同样先获取当前线程的 ThreadLocalMap 对象,然后根据 ThreadLocal 实例对象查找对应的变量副本。 - ⑤ ThreadLocalMap 内部的键值对使用 ThreadLocal 的弱引用作为键,这样可以避免内存泄漏,当 ThreadLocal 对象被垃圾回收时,对应的键值对也会被自动清除。
- ⑥ ThreadLocalMap 内部使用开放寻址法来解决哈希冲突,具体来说,它将键值对存储在一个数组
Entry[]
中,通过哈希值计算得到数组索引,如果该位置已经被占用,则向后探测,直到找到一个空闲位置或者找遍整个数组。 - ⑦ 在多线程环境下,每个线程都独立地访问和操作自己的 ThreadLocalMap 对象,这样就实现了线程之间的隔离,每个线程都有自己独立的变量副本,互不干扰。
- ⑧ 当线程结束时,由于 ThreadLocalMap 是与线程绑定的,因此对应的 ThreadLocalMap 对象会随着线程的销毁而被垃圾回收,从而释放相关的资源。
综上来说,ThreadLocal 的实现原理是通过在每个 Thread 对象内部维护一个 ThreadLocalMap 来存储线程本地变量,使用了自定义的哈希表结构和弱引用。这种机制使得每个线程都可以独立地访问和操作自己的变量副本,从而实现了线程之间的隔离和线程安全性。
注意: 由于 ThreadLocal 的实现原理使用了哈希表和自定义的数据结构,所以在大量线程和大量 ThreadLocal 对象同时存在的情况下,可能会导致内存消耗较大。因此,在使用 ThreadLocal 时,需要注意内存管理和合理使用的问题。
六、ThreadLocal 源码 - 属性
1/**
2 * 该属性的作用是作为 Hash 计算的底数
3 *
4 * 用户 Hash 计算的底数,在计算线程本地哈希值时混入一个随机数,从而提高哈希值的分布性减少哈希冲突。在 Java
5 * 的线程池中,每个线程都有一个唯一的 Thread ID (线程ID),同时又有一个 ThreadLocal 哈希值,它是由当前线
6 * 程池的 HASH_INCREMENT 值加上线程 ID 计算得出的。因此使用 HASH_INCREMENT 可以保证生成的哈希值是具有
7 * 一定随机性和唯一性的,可以有效降低哈希冲突的概率,提高哈希表的性能。
8 */
9private static final int HASH_INCREMENT = 0x61c88647;
10
11/**
12 * 该属性的作用是用于生成唯一的 HashCode 值分配给 ThreadLocal 实例的 threadLocalHashCode 属性
13 *
14 * nextHashCode() 方法会先获取 nextHashCode 变量的值,然后使用 CAS 操作,将其更新为原值加上固定的增量
15 * (0x61c88647),最后返回更新后的值。这个增量是随机的,它的目的是使生成的 hash code 分布得更加均匀,以
16 * 减少 hash 冲突的概率。由于使用了 CAS 操作,因此可以保证在多线程环境下唯一性和线程安全性。
17 *
18 * 因此,nextHashCode 属性实际上是 ThreadLocal 类用于生成唯一的 hash code 的关键属性之一,它保证了
19 * 每个 ThreadLocal 实例都拥有唯一的 threadLocalHashCode 值,从而保证了多个线程使用 ThreadLocal
20 * 实例时的独立性和安全性。
21 */
22private static AtomicInteger nextHashCode = new AtomicInteger();
23
24/**
25 * 该属性的作用是用于分配给每个线程的 ThreadLocal 独一无二的 HashCode 值
26 *
27 * 在 ThreadLocal 类中每个 ThreadLocal 实例都有一个 threadLocalHashCode 属性,当线程通过 ThreadLocal
28 * 对象访问本地变量时,ThreadLocal 对象会根据当前线程的 threadLocalHashCode 查找该线程的 ThreadLocalMap
29 * 对象,并通过 ThreadLocalMap 对象获取与该 ThreadLocal 对象关联的本地变量的值。
30 *
31 * 由于每个线程的 threadLocalHashCode 是独一无二的,因此可以确保每个线程都能安全地访问自己的本地变量。因此
32 * threadLocalHashCode 属性实际上是 ThreadLocal 类用于实现线程本地变量的关键属性之一,它保证了每个线程本
33 * 地变量的独立性和安全性。
34 */
35private final int threadLocalHashCode = nextHashCode();
七、ThreadLocal 源码 - 方法
7.1 构造方法 ThreadLocal
1/**
2 * ThreadLocal 构造方法
3 */
4public ThreadLocal() {
5}
7.2 获取数据方法 get()
get(): 获取当前线程中 ThreadLocal 对象的值。
1/**
2 * 获取当前线程中 ThreadLocal 对象的值
3 *
4 * 获取当前线程中 ThreadLocal 对象关联的值,每个线程获取的都是属于各自线程的 ThreadLocal 对象关联的值,从
5 * 而实现了多线程之间的数据隔离。当然,如果当前线程没有设置值,那么当前线程在调用 get() 方法获取值时就会调
6 * 用 initialValue() 方法初始化一个值。
7 *
8 * @return 当前线程中 ThreadLocal 对象关联的值。
9 */
10public T get() {
11 // 获取当前线程对象
12 Thread t = Thread.currentThread();
13 // 调用 getMap() 方法获取当前线程的 ThreadLocalMap 对象
14 ThreadLocalMap map = getMap(t);
15 // 判断当前线程的 ThreadLocalMap 对象是否为空
16 if (map != null) {
17 // 调用 ThreadLocalMap 的 getEntry() 方法,获取与当前 ThreadLocal 对象相关联的 Entry 对象
18 ThreadLocalMap.Entry e = map.getEntry(this);
19 // 判断获取到的 Entry 对象是否为空,如果不为空则将得到的值强制转换为泛型 T,然后返回转换后的值
20 if (e != null) {
21 @SuppressWarnings("unchecked")
22 T result = (T)e.value;
23 return result;
24 }
25 }
26 // 若前面的步骤都没有获取到值,则调用 setInitialValue() 方法设置一个初始值,并返回该值。
27 return setInitialValue();
28}
setInitialValue(): 设置当前线程 ThreadLocal 对象的初始值。
1/**
2 * 设置当前线程 ThreadLocal 对象的初始值
3 *
4 * @return 当前线程 ThreadLocal 对象的初始值。
5 */
6private T setInitialValue() {
7 // 调用 initialValue() 方法生成一个初始值
8 T value = initialValue();
9 // 获取当前线程对象
10 Thread t = Thread.currentThread();
11 // 调用 getMap() 方法获取当前线程的 ThreadLocalMap 对象
12 ThreadLocalMap map = getMap(t);
13 // 判断当前线程的 ThreadLocalMap 对象是否为空
14 // - 如果当前线程的 ThreadLocalMap 对象不为空,则调用 set() 方法为 ThreadLocal 对象设置值。
15 // - 如果当前线程的 ThreadLocalMap 对象为空,则调用 createMap() 方法创建一个 ThreadLocalMap 对象,
16 // 并将 ThreadLocal 对象设置到其中。
17 if (map != null) {
18 map.set(this, value);
19 } else {
20 createMap(t, value);
21 }
22 // 判断当前 ThreadLocal 对象是否是 TerminatingThreadLocal 类的实例。
23 if (this instanceof TerminatingThreadLocal) {
24 // 果当前 ThreadLocal 对象是 TerminatingThreadLocal 类的实例,则调用 register() 方法
25 // 将其注册到 TerminatingThreadLocal 的列表中,以便在线程结束时执行相应的操作。
26 TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
27 }
28 return value;
29}
7.3 移除数据方法 remove()
1/**
2 * 删除当前线程中 ThreadLocal 对象所对应的值
3 *
4 * 调用 remove() 方法后,当前线程 ThreadLocal 对象不再持有任何值,下次访问该线程局部变量时将返回其初始值
5 * (如果有的话) 或者 null。该方法的主要作用是清除当前线程中的线程局部变量的值,以防止内存泄漏或不必要的持有。
6 *
7 * 注意:
8 * ① 如果线程结束或线程被回收,线程局部变量的值也会被自动移除,因此通常不需要显式调用 remove() 方法。然而在
9 * 某些情况下,当线程仍然活动时则需要手动清除线程局部变量的值,以避免潜在的问题。
10 * ② remove() 方法是在当前线程中进行清除操作的,而不是在其他线程中进行。因此当多个线程同时使用同一个 ThreadLocal
11 * 变量时,每个线程都需要进行 remove() 操作,以确保 ThreadLocal 变量和对应的值能够被正确清除。
12 */
13public void remove() {
14 // 调用 getMap() 方法获取当前线程的 ThreadLocalMap 对象
15 ThreadLocalMap m = getMap(Thread.currentThread());
16 // 判断当前线程的 ThreadLocalMap 对象是否为 null
17 if (m != null) {
18 // 如果不为 null 则从 ThreadLocalMap 中移除当前线程中 ThreadLocal 对象所对应的值
19 m.remove(this);
20 }
21}
7.4 保存数据方法 set(T value)
1/**
2 * 设置当前线程中 ThreadLocal 对象所对应的值
3 *
4 * 在使用 ThreadLocal 对象时每个线程都会维护一个 ThreadLocalMap 对象,并在该对象中存储以 ThreadLocal
5 * 对象做为 Key,然后存储与该线程的 ThreadLocal 对象所对应的值。当调用 ThreadLocal 的 set() 方法时,
6 * 会先获取当前线程的 ThreadLocalMap 对象,然后将该 ThreadLocal 对象与指定的值作为键值对,存储到
7 * ThreadLocalMap 中。
8 *
9 * 由于 ThreadLocal 对象是线程隔离的,即每个线程都有自己的 ThreadLocalMap 对象,因此在多线程环境中,
10 * 通过 ThreadLocal 对象可以轻松地实现线程间的变量传递,而不需要使用 synchronized 等线程同步机制,
11 * 从而提高程序的并发性能。
12 *
13 * 需要注意的是,在使用 ThreadLocal 对象时建议在使用结束后尽早清除该对象对应的值,可以使用 ThreadLocal
14 * 的 remove()方法进行清除。如果忘记清除 ThreadLocal 对象的值,则有可能会导致内存泄漏等问题。
15 *
16 * @param value 要存储在当前线程的值
17 */
18public void set(T value) {
19 // 获取当前线程对象
20 Thread t = Thread.currentThread();
21 // 调用 getMap() 方法获取当前线程的 ThreadLocalMap 对象
22 ThreadLocalMap map = getMap(t);
23 // 判断 ThreadLocalMap 是否为空
24 // - 如果不为 null,则调用 set() 方法将当前 ThreadLocal 对象和值存储在映射中;
25 // - 如果为 null,则调用 createMap() 方法创建一个新的 ThreadLocalMap 对象,
26 // 并将当前 ThreadLocal 对象和值存储在其中,并将其与当前线程关联;
27 if (map != null) {
28 map.set(this, value);
29 } else {
30 createMap(t, value);
31 }
32}
7.5 初始化 ThreadLocal 值方法 initialValue()
1/**
2 * 初始化 ThreadLocal 值
3 *
4 * 在使用 ThreadLocal 变量时,如果当前线程尚未为该变量设置过值,则会调用该方法为其设置一个初始值。一般情
5 * 况下我们以定义一个 ThreadLocal 变量,然后继承 ThreadLocal 类并覆盖 initialValue() 方法,来自定
6 * 义 ThreadLocal 变量的初始值。
7 *
8 * @return 初始化的 ThreadLocal 值。
9 */
10protected T initialValue() {
11 return null;
12}
7.6 生成下一个 HashCode 值方法 nextHashCode()
1/**
2 * 生成下一个 HashCode 值
3 */
4private static int nextHashCode() {
5 return nextHashCode.getAndAdd(HASH_INCREMENT);
6}
7.7 获取 ThreadLocalMap 对象方法 getMap(Thread t)
1/**
2 * 获取一个线程的 ThreadLocalMap 对象
3 *
4 * @param t 线程对象 (默认是当前线程)
5 * @return 线程关联的 ThreadLocalMap 集合
6 */
7ThreadLocalMap getMap(Thread t) {
8 // 获取指定线程的 ThreadLocalMap 对象
9 return t.threadLocals;
10}
7.8 创建继承父线程 ThreadLocalMap 副本方法 createInheritedMap()
1/**
2 * 创建一个继承父线程的 ThreadLocalMap 的副本
3 *
4 * 当一个线程创建子线程时,子线程通常会继承父线程的 ThreadLocal 变量。而 createInheritedMap 方法的作用是在子线程
5 * 中创建一个与父线程中相同的 ThreadLocalMap 对象,并将父线程的 ThreadLocalMap 对象中的键值对复制到新的对象中去。
6 * 这样子线程就可以在自己的 ThreadLocalMap 中找到与父线程相同的 ThreadLocal 变量,并进行访问和修改。
7 *
8 * 这个方法通常在创建子线程时由 JVM 内部调用,以确保子线程继承了父线程的 ThreadLocal 变量。
9 *
10 * @param parentMap 与父线程关联的 ThreadLocalMap
11 * @return 包含父级可继承绑定的 ThreadLocalMap
12 */
13static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
14 return new ThreadLocalMap(parentMap);
15}
7.9 创建 ThreadLocalMap 对象方法 createMap(Thread t, T firstValue)
1/**
2 * 创建并初始化当前线程的 ThreadLocalMap 对象。
3 *
4 * 在调用 createMap 方法后,首先会创建一个新的 ThreadLocalMap 对象,并将该对象赋给当前线程的 threadLocals 变量。
5 * 然后将指定的ThreadLocal 对象和对应的初始值放入 ThreadLocalMap 中。
6 *
7 * @param t 当前线程
8 * @param firstValue ThreadLocalMap 集合中要插入的第一个值
9 */
10void createMap(Thread t, T firstValue) {
11 t.threadLocals = new ThreadLocalMap(this, firstValue);
12}
八、ThreadLocalMap 源码 - 属性
1/**
2 * ThreadLocalMap 初始容量
3 */
4private static final int INITIAL_CAPACITY = 16;
5
6/**
7 * 底层数据结构,使用数组表示存储 Entry 对象。
8 */
9private Entry[] table;
10
11/**
12 * 集合中 Entry 对象数量
13 */
14private int size = 0;
九、ThreadLocalMap 源码 - 方法
9.1 构造方法 ThreadLocalMap
(1) ThreadLocalMap 构造方法
1/**
2 * ThreadLocalMap 构造方法
3 *
4 * 构造一个包含初始 Entry(firstKey, firstValue) 的新的 ThreadLocalMap。因为 ThreadLocalMap 是惰性构造的,
5 * 所以需要至少存入一个 Entry 对象时,才执行创建操作。
6 */
7ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
8 // 创建一个长度为 INITIAL_CAPACITY 的 Entry 类型数组 table,用于存储 ThreadLocal 与其对应值的映射关系
9 table = new Entry[INITIAL_CAPACITY];
10 // 将分配给 ThreadLocal 对象的 HashCode 值与 INITIAL_CAPACITY-1 进行与运算,计算第一个 Entry 对象存放的位置
11 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
12 // 创建 Entry 对象,并将其存储在 table 数组中 i 的位置
13 table[i] = new Entry(firstKey, firstValue);
14 // 将 ThreadLocalMap 集合大小设置为 1
15 size = 1;
16 // 根据数组长度和装载因子,计算并设置扩容阈值
17 setThreshold(INITIAL_CAPACITY);
18}
(2) 据父线程关联的 ThreadLocalMap 创建 ThreadLocalMap 的构造方法
1/**
2 * 根据父线程关联的 ThreadLocalMap 创建 ThreadLocalMap 的构造方法
3 *
4 * 构造一个新 ThreadLocalMap,包括来自与父线程关联的 ThreadLocalMap 中所有可继承 ThreadLocal 对象。
5 *
6 * @param parentMap 与父线程关联的 ThreadLocalMap
7 */
8private ThreadLocalMap(ThreadLocalMap parentMap) {
9 // 获取父线程的 ThreadLocalMap 中的 table 数组并赋给变量 parentTable
10 Entry[] parentTable = parentMap.table;
11 // 使用 len 变量记录父线程 ThreadLocalMap 中数组 table 的长度
12 int len = parentTable.length;
13 // 根据父线程 table 的长度 len 计算新建的当前线程 ThreadLocal 关联的 ThreadLocalMap 的阈值
14 setThreshold(len);
15 // 创建新的长度为 len 的 Entry 数组作为当前当前线程 ThreadLocal 关联的 ThreadLocalMap 的 table 数组
16 table = new Entry[len];
17
18 // 遍历父线程的 parentTable 数组
19 for (Entry e : parentTable) {
20 // 判断数组中的元素 Entry 是否为空
21 if (e != null) {
22 @SuppressWarnings("unchecked")
23 // 获取父类线程 Entry 对象的 key (即ThreadLocal对象)
24 ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
25 // 如果 key 不为 null,则执行下面操作:
26 if (key != null) {
27 // 将该父类 Entry 中的值赋给变量 value;
28 Object value = key.childValue(e.value);
29 // 创建 Entry 对象并设置对象的 key 和 value 都和父类 Entry 对象的 key 和 value 保持一致;
30 Entry c = new Entry(key, value);
31 // 计算存储新创建的 Entry 对象在数组 table 中的位置
32 int h = key.threadLocalHashCode & (len - 1);
33 // 判断存储新创建 Entry 对象的数组位置是否为 null,如果不是则说明发生哈希冲突,使用开放寻址法
34 // 继续向后寻找空的位置,找到后将对象 Entry 存入到该位置。
35 while (table[h] != null) {
36 h = nextIndex(h, len);
37 }
38 table[h] = c;
39 // 使当前线程的 ThreadLocal 关联的 ThreadLocalMap 的大小 +1。
40 size++;
41 }
42 }
43 }
44}
9.2 扩容方法 rehash()
rehash(): ThreadLocalMap 扩容方法。
1/**
2 * ThreadLocalMap 扩容方法
3 */
4private void rehash() {
5 // 先调用 expungeStaleEntries() 方法来清除 ThreadLocalMap 中过期的 Entry 对象,避免内存泄漏
6 expungeStaleEntries();
7
8 // ThreadLocalMap 的负载因子已经超过了阈值的 75%,那么就将哈希表的长度加倍。这里为了避免阈值的惯性滞
9 // 后效应,使用了 25% 的下调阈值大小,这样在扩容后就可以保证下一次再进行扩容时不必等到 ThreadLocalMap
10 // 的所有条目都被填满才开始扩容。
11 if (size >= threshold - threshold / 4) {
12 // 执行真正执行扩容操作的方法
13 resize();
14 }
15}
expungeStaleEntries(): 清除 ThreadLocalMap 中过期的 Entry 对象。
1/**
2 * 清除 ThreadLocalMap 中过期的 Entry 对象
3 *
4 * 由于 ThreadLocalMap 中的 Entry 对象是使用 WeakReference 弱引用来实现的,因此当 ThreadLocal 对象被回
5 * 收后,对应的 Entry 对象也会被回收,但是由于 WeakReference 的特殊性质,这个对象有可能并没有被及时回收掉,
6 * 因此需要定期清理。而该方法就是用于清理这些过期的 Entry 对象的,执行时会遍历 ThreadLocalMap 中所有的 Entry
7 * 对象,找到已经过期的 Entry 对象,并将其清除,从而保证 ThreadLocalMap 中只保留当前还有用的 Entry 对象。
8 */
9private void expungeStaleEntries() {
10 // 获取当前 Map 中的 Entry 数组
11 Entry[] tab = table;
12 // 获取 Entry 数组的长度
13 int len = tab.length;
14 // 遍历所有的 Entry
15 for (int j = 0; j < len; j++) {
16 // 获取当前位置的 Entry
17 Entry e = tab[j];
18 // 判断 Entry 是否为 null 并且其 key 对应的对象已经被 GC 回收
19 // 如果是则调用 expungeStaleEntry 方法删除该 Entry
20 if (e != null && e.get() == null) {
21 expungeStaleEntry(j);
22 }
23 }
24}
9.3 计算下个索引位置方法 nextIndex(int i, int len)
1/**
2 * 计算下一个索引位置,当前方法用于实现 ThreadLocalMap 中 index 计数器的递增和循环使用
3 *
4 * 该方法接收两个参数,分别是:
5 * - i: 表示当前的 index 值;
6 * - len: 表示数组的长度;
7 * 在当前方法中,先将 i+1 的值与 len 取模,然后返回计算后的结果。如果 i+1 的值等于 len 则执行
8 * nextIndex(i, len) 返回 0,表示 ThreadLocalMap 的 index 计数器循环中断,需要重置为 0,
9 * 以便接下来的使用。通过当前方法 ThreadLocalMap 可以实现递增计数器并象征性循环使用 index 计
10 * 数器的功能。
11 */
12private static int nextIndex(int i, int len) {
13 return ((i + 1 < len) ? i + 1 : 0);
14}
9.4 计算上个索引位置方法 prevIndex(int i, int len)
1/**
2 * 计算上一个索引位置,当前方法用于实现 ThreadLocalMap 中 index 计数器的递减和循环使用
3 *
4 * 该方法同样接收两个参数,分别是:
5 * - i: 表示当前的 index 值;
6 * - len: 表示数组的长度;
7 * 在当前方法中,先将 i-1 的值与 0 比较,如果 i-1 大于等于 0 则返回计算后的结果,否则返回 len-1。
8 * 如果 i-1 的值为 0 则返回 len-1,表示 ThreadLocalMap 的 index 计数器循环中断,需要重置为
9 * len-1,以便接下来的使用。通过当前方法 ThreadLocalMap 可以实现递减计数器并循环使用 index 计
10 * 数器的功能。
11 */
12private static int prevIndex(int i, int len) {
13 return ((i - 1 >= 0) ? i - 1 : len - 1);
14}
9.5 获取 Entry 对象方法 getEntry(ThreadLocal<?> key)
getEntry(ThreadLocal<?> key): 获取给定 ThreadLocal 对象的 Entry 对象。
1/**
2 * 获取给定 ThreadLocal 对象的 Entry 对象
3 *
4 * Entry 对象是 ThreadLocalMap 类中的一个内部类,由键值对构成,其 Key 是弱引用的 ThreadLocal 对象,其值为
5 * Object 类型。当一个 ThreadLocal 对象没有被其它对象所引用时,它就会被垃圾回收器回收,并且对应的 Entry 对象
6 * 也会被删除。
7 *
8 * @param key 与线程关联的 ThreadLocal 对象
9 * @return 如果查找到则返回对应的 Entry 对象,如果未找到则返回 null
10 */
11private Entry getEntry(ThreadLocal<?> key) {
12 // 获取 ThreadLocalMap 的 HashCode,然后使该 HashCode 与 table 数组长度进行与计算,计算出其在数组中的位置
13 int i = key.threadLocalHashCode & (table.length - 1);
14 // 获取 table 数组中计算位置的 Entry 对象
15 Entry e = table[i];
16 // 判断 Entry 对象是否不为 null,并且 Entry 的 key 和传入的 key 一致:
17 // - 如果满足条件,则返回该 Entry 对象;
18 // - 如果不满足条件,则调用 getEntryAfterMiss 方法,从 i 位置向后继续查找 Entry 对象;
19 if (e != null && e.get() == key) {
20 return e;
21 } else {
22 return getEntryAfterMiss(key, i, e);
23 }
24}
getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e): 在哈希冲突的情况下,查找指定 ThreadLocal 对象对应的 Entry 对象。
1/**
2 * 该方法用于在哈希冲突的情况下,查找指定 ThreadLocal 对象对应的 Entry 对象
3 *
4 * @param key 要查找的 ThreadLocal 对象
5 * @param i 指定 ThreadLocal 对象在数组中的下标位置
6 * @param e 数组 i 位置存储的 Entry 对象
7 * @return 如果查找到则返回对应的 Entry 对象,如果未找到则返回 null
8 */
9private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
10 // 将 ThreadLocalMap 中的数组 table 赋给变量 tab
11 Entry[] tab = table;
12 // 使用 len 变量记录 ThreadLocalMap 中数组 table 的长度
13 int len = tab.length;
14 // 循环查找指定 ThreadLocal 对象对应的 Entry 对象
15 while (e != null) {
16 ThreadLocal<?> k = e.get();
17 // 如果 k 和要查找的 key 相同,则说明找到了对应的 Entry 对象,直接将该对象返回
18 if (k == key) {
19 return e;
20 }
21 // 如果 Entry 对象的 key 为 null,则说明当前 Entry 已经过期,直接调用 expungeStaleEntry 清除该对象
22 if (k == null) {
23 expungeStaleEntry(i);
24 }
25 // 指向下个位置
26 else {
27 i = nextIndex(i, len);
28 }
29 // 将下个位置 i 中的 Entry 对象赋给变量 e
30 e = tab[i];
31 }
32 // 如果遍历到最后都没有找到,则默认返回 null
33 return null;
34}
9.6 删除 Entry 对象方法 remove(ThreadLocal<?> key)
1/**
2 * 从当前线程的 ThreadLocalMap 中根据传入的 ThreadLocal 对象删除指定的 Entry
3 *
4 * @param key 需要删除的 ThreadLocal 对象
5 */
6private void remove(ThreadLocal<?> key) {
7 // 将 ThreadLocalMap 中的数组 table 赋给变量 tab
8 Entry[] tab = table;
9 // 使用 len 变量记录 ThreadLocalMap 中数组 table 的长度
10 int len = tab.length;
11 // 将传入的 ThreadLocal 对象的 HashCode 与 len-1 进行按位与运算,计算出该对象 Entry 在数组中的下标位置
12 int i = key.threadLocalHashCode & (len-1);
13 // 从 table 数组中的 i 位置开始,往后遍历查找与传入 key 相同的 entry 对象。这里之所以是从 i 位置开始往后遍
14 // 历查找,主要是因为 ThreadLocalMap 中解决哈希冲突的方法为开放寻址法,即插入数据发生哈希冲突时,继续往后寻
15 // 找空的位置来存放该插入的数据,这样来解决哈希冲突。所以,查找数据时也需要从计算出的位置,往后遍历查找才行。
16 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
17 // 判断 Entry 的 key 是否和传入的 key 相同,如果相同则清除该 Entry 对象
18 if (e.get() == key) {
19 // 设置 Entry 的 key ThreadLocal 对象的引用为 null,便于下次 GC 回收该对象
20 e.clear();
21 // 清除 Entry 对象
22 expungeStaleEntry(i);
23 return;
24 }
25 }
26}
9.7 清除过期 Entry 的方法 expungeStaleEntry(int staleSlot)
1/**
2 * 执行清除当前线程 ThreadLocal 对象中过期 Entry 的方法
3 *
4 * @param staleSlot 需要清除的数组下标位置
5 * @return 最后一个遍历到的数组下标
6 */
7private int expungeStaleEntry(int staleSlot) {
8 // 将 ThreadLocalMap 中的数组 table 赋给变量 tab
9 Entry[] tab = table;
10 // 使用 len 变量记录 ThreadLocalMap 中数组 table 的长度
11 int len = tab.length;
12
13 // 将 staleSlot 位置的 Entry 对象的值设置为 null
14 tab[staleSlot].value = null;
15 // 将 staleSlot 位置的 Entry 对象设置为 null
16 tab[staleSlot] = null;
17 // 将 ThreadLocalMap 大小减少 1
18 size--;
19
20 // 定义变量 e 来记录当前正在遍历的 Entry 对象
21 Entry e;
22 // 定义一个局部变量 i 来记录当前正在遍历的数组下标
23 int i;
24 // 循环遍历数组,从 staleSlot 的下一个位置开始。
25 // 如果当前位置的 Entry 对象不为 null,则使用变量 e 记录该对象,并将下标 i 更新为下一个不为 null 的位置。
26 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
27 // 获取 e 对象中存储 key,即 ThreadLocal 对象,将其赋给变量将 k
28 ThreadLocal<?> k = e.get();
29 // 判断 ThreadLocal 对象是否为 null:
30 // - 如果是 null,则表示该 entry 已经过期,需要将 value 和数组下标位置都设置为 null;
31 // - 如果不是 null,则表示该条目未过期,需要检查其存储位置是否正确;
32 if (k == null) {
33 // 将 e 对象的 value 设置为 null
34 e.value = null;
35 // 将 tab 数组中下标为 i 的元素设置为 null
36 tab[i] = null;
37 // 将 ThreadLocal 关联的 ThradLocalMap 大小减 1
38 size--;
39 } else {
40 // 根据 ThreadLocal 对象的 hashCode 值计算出该对象应该存储在数组中的位置 h
41 int h = k.threadLocalHashCode & (len - 1);
42 // 如果 h 与当前位置 i 不同,则表示该 Entry 对象存储的位置不正确
43 if (h != i) {
44 // 将当前位置 i 的元素设置为 null
45 tab[i] = null;
46 // 从位置 h 开始向后遍历数组,直到找到一个空闲的位置,然后将该位置赋给变量 h
47 while (tab[h] != null) {
48 h = nextIndex(h, len);
49 }
50 // 将 e 对象存储在数组空闲位置 h 中
51 tab[h] = e;
52 }
53 }
54 }
55 // 返回最后一个遍历到的数组下标
56 return i;
57}
9.8 设置与线程 ThreadLocal 关联的值方法 set(ThreadLocal<?> key, Object value)
set(ThreadLocal<?> key, Object value): 设置与当前线程 ThreadLocal 对象关联的值。
1/**
2 * 设置与当前线程 ThreadLocal 对象关联的值
3 *
4 * 如果当前线程还没有该 ThreadLocal 对象的值则将值设置到该对象中,否则将新值替换为当前值。
5 *
6 * 该方法内部会先获取当前线程的 ThreadLocalMap 对象,然后通过 key 的 hashCode() 方法获取 HashCode,再根据 HashCode
7 * 在 ThreadLocalMap 对象中查找对应 Entry 对象。如果该 Entry 对象为空,则创建一个新的 Entry 对象并添加到 ThreadLocalMap
8 * 对象中,否则将新值设置到 Entry 对象中。
9 *
10 * @param key 当前线程 ThreadLocal 对象
11 * @param value 与 ThreadLocal 对象关联的值
12 */
13private void set(ThreadLocal<?> key, Object value) {
14 // 将 ThreadLocalMap 中的数组 table 赋给变量 tab
15 Entry[] tab = table;
16 // 使用 len 变量记录 ThreadLocalMap 中数组 table 的长度
17 int len = tab.length;
18 // 使用线程的 ThreadLocal 中的 HashCode 值与 len-1 进行与操作,计算出当前 Entry 要存储在数组中的下标位置
19 int i = key.threadLocalHashCode & (len-1);
20
21 // 执行循环,从数组 i 位置开始获取该位置中对应的 Entry 对象,然后进行下面的一些判断操作,如果本次循环没有执
22 // 行 return,则获取下一个位置的 Entry 对象进行判断,直至某次循环过程中执行 return 返回,或者到末尾后 Entry
23 // 对象为 null,才会终止循环。
24 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
25 // 获取当前位置的 Entry 对象中的 key,即 ThreadLocal 对象
26 ThreadLocal<?> k = e.get();
27 // 如果当前 k 就是要查找的 key,相当于找到了对应的 ThreadLocal 对象所在下标位置,将该 Entry 对象的值设置
28 // 为传入的 value,然后返回。
29 if (k == key) {
30 e.value = value;
31 return;
32 }
33 // 如果 Entry 对象的 key 为 null,则说明当前 Entry 已经过期,直接调用 expungeStaleEntry 清除该对象,然
34 // 后方法返回。
35 if (k == null) {
36 replaceStaleEntry(key, value, i);
37 return;
38 }
39 }
40 // 如果走到这一步,相当于目前当前线程 ThreadLocalMap 中不存在用该 ThreadLocal 对象作为 key 的 Entry。所以,这
41 // 里新创建一个 Entry 对象,并将传入的 key 和 value 作为新 Entry 的 key 和 value。
42 tab[i] = new Entry(key, value);
43 // 使 ThreadLocalMap 大小+1
44 int sz = ++size;
45 // 清理一下从 i 位置到 sz 位置中的过期的 Entry 对象,然后判断 sz 大小是否达到了设置的阈值 threshold,如果达到了
46 // 就调用 rehash 方法尝试扩容。
47 if (!cleanSomeSlots(i, sz) && sz >= threshold) {
48 rehash();
49 }
50}
replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot): 将一个已经过期的 Entry 替换为一个新的 Entry。
1/**
2 * 该方法主要作用是将一个已经过期的 Entry 替换为一个新的 Entry
3 *
4 * 该方法会在 ThreadLocalMap 中查找 key 对应的 Entry 对象,查找是否存在:
5 * - 如果存在,则将 value 赋值给该 Entry 对象的 value 属性,然后清理过期 Entry 对象工作;
6 * - 如果不存在,则会创建一个新的 Entry 对象,并将其插入到 staleSlot 指定的位置,同时将 ThreadLocalMap 中
7 * 对应的 Entry 对象标记为过期,最好执行清理 Entry 工作。
8 * 该方法的作用是保证当 ThreadLocal 对象对应的 Entry 过期后,下一次访问该 ThreadLocal 对象时能够正确地插入
9 * 新的值。在并发场景下,多线程可能同时访问和修改 ThreadLocalMap 对象,如果没有正确地处理过期的条目,可能会导
10 * 致程序出现非预期的行为。因此该方法在保证线程安全性的同时,也保证了 ThreadLocal 对象的正确性。
11 *
12 * @param key 需要设置的键 (表示要操作的 ThreadLocal 对象)
13 * @param value 需要设置的值 (表示与对应 ThreadLocal 关联的值)
14 * @param staleSlot 表示需要替换的过期的 Entry 位置
15 */
16private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
17 // 将 ThreadLocalMap 中的数组 table 赋给变量 tab
18 Entry[] tab = table;
19 // 使用 len 变量记录 ThreadLocalMap 中数组 table 的长度
20 int len = tab.length;
21 // 定义变量 e 来记录当前正在遍历的 Entry 对象
22 Entry e;
23
24 // 定义变量 slotToExpunge 记录需要清除的位置
25 int slotToExpunge = staleSlot;
26 // 向前遍历数组,直到遍历到 Entry 为 null 时结束。
27 // --- 注: 遍历过程主要是为了查找已经过期的 Entry (key为null就是过期),如果遇到过期的 Entry 就记录下该 Entry
28 // 所在的数组下标位置。如果遍历过程中存在多个过期的 Entry,那么只记录最后一个过期 Entry 所在的数组下标位置。
29 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) {
30 if (e.get() == null) {
31 slotToExpunge = i;
32 }
33 }
34
35 // 向后遍历数组,直到遍历到 Entry 为 null,或者遇到 Entry 的 key 和传入的 key 相同时结束。
36 // --- 注: 遍历过程主要是为了寻找需要被清理的位置,便于执行清理过期 Entry。
37 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
38 // 获取 e 对象中存储 key,即 ThreadLocal 对象,将其赋给变量将 k
39 ThreadLocal<?> k = e.get();
40
41 // 如果当前位置 Entry 对象的 key 和传入的 key 相同,则:
42 // ① 在直接将当前的 Entry 对象的值设置为传入的 value;
43 // ② 将当前位置与过期 Entry 的位置进行交换,以保持哈希表的顺序;
44 // ③ 判断 slotToExpunge 是否等于传入的过期 Entry 所在位置 staleSlot,是则设置变量 slotToExpunge
45 // 中记录的需要被清理位置为 i;
46 // ④ 执行 Entry 的清理清理工作;
47 if (k == key) {
48 // ①
49 e.value = value;
50 // ②
51 tab[i] = tab[staleSlot];
52 tab[staleSlot] = e;
53 // ③
54 // 注: 之所以设置 slotToExpunge = i,主要是在 i 和 staleSlot 位置的 Entry 进行了交换,为了保证变量
55 // slotToExpunge 中记录的清理位置的正确性,需要将变量 slotToExpunge 中记录的位置 staleSlot 变为位
56 // 置 i,因为交换后位置 i 才是那个已经过期需要被回收的 Entry 对象。
57 if (slotToExpunge == staleSlot) {
58 slotToExpunge = i;
59 }
60 // ④
61 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
62 return;
63 }
64
65 // 如果当前位置 Entry 对象的 key 为 null,则说明当前 Entry 已经过期,再判断 slotToExpunge 中记录的清除
66 // 的位置是否是传入的过期 Entry 所在位置,如果是则设置变量 slotToExpunge 中记录的清除的位置为当前位置 i。
67 if (k == null && slotToExpunge == staleSlot) {
68 slotToExpunge = i;
69 }
70 }
71
72 // 如果传入的 key 所在位置没有找到,则创建一个新的 Entry 对象插入到数组 stableSlot 位置。
73 tab[staleSlot].value = null;
74 tab[staleSlot] = new Entry(key, value);
75
76 // 如果记录的需要清除的位置并不是传入的过期 Entry 所在位置 staleSlot,那么说明还存其它过期 Entry,直接对这些
77 // Entry 所在位置进行清理。
78 if (slotToExpunge != staleSlot) {
79 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
80 }
81}
十、ThreadLocal 相关问题
10.1 ThreadLocal 使用时应该注意哪些问题?
在使用 ThreadLocal 时,需要注意以下几个问题:
- ① 初始化值: ThreadLocal 的
initialValue()
方法用于初始化线程本地变量的值。如果没有显式调用set()
方法设置值,则会使用initialValue()
方法提供的初始值。在使用initialValue()
方法时,需要注意返回的是全新的对象副本,而不是共享对象。 - ② 内存泄漏: 由于 ThreadLocalMap 的生命周期与线程相同,如果没有手动清理 ThreadLocal 对象的引用,就有可能导致内存泄漏。在使用完 ThreadLocal 后,应该调用
remove()
方法或设置为 null,以便及时释放 ThreadLocal 对象的引用。 - ③ 内存溢出: 由于每个线程都有一个独立的 ThreadLocalMap,如果线程数过多,就会导致创建的 Map 过多,占用大量内存,可能会导致堆内存溢出。
- ④ 线程安全性: 虽然 ThreadLocal 可以提供线程间的隔离,但在多线程环境下,依然需要保证线程安全性。如果多个线程共享某个对象的 ThreadLocal 变量副本,并且对该变量进行修改操作,仍然需要采取额外的同步措施。
- ④ 线程池使用时的清理: 在使用线程池时,由于线程的复用性,ThreadLocal 的值可能会被保留下来并影响下一次任务的执行。因此,使用完 ThreadLocal 后,需要显式调用
remove()
方法或设置为 null,以便清理线程池中的线程的 ThreadLocal 值。
10.2 ThreadLocal 为什么会出现内存泄漏问题?
ThreadLocal 底层是使用各个线程自身的 ThreadLocalMap 存储线程各自的数据,而 ThreadLocalMap 中使用 Entry[] 数组存储着键值对对象 Entry,这里 Entry.key 是一个 ThreadLocal 的弱引用对象,而 Entry.value 是一个存储线程数据的强引用对象。
当系统执行 GC 时,就会将 Entry.key 弱引用的对象 ThreadLocal 给回收掉,而 Entry.value 强引用对象却不会被回收,造成没有办法通过 Entry.key 去访问 Entry.value。遇到这种情况时,如果线程再迟迟不结束的话,这些 Entry.key 为 null 的 Entry.value 就会一直存在一条强引用链,导致 ThreadLocalMap 中的 Entry 对象无法被回收,造成内存泄漏。引用关系如下:
- Thread -> ThreaLocalMap -> Entry[] -> Entry -> value
其实,在 ThreadLocalMap 的设计中已经考虑到这种情况,也加上了一些防护措施。比如,在调用 在ThreadLocal 中的 get()
或 set()
方法时,会触发 ThreadLocal 清理无效的 Entry 对象机制,但是这些防护措施并不能保证一定不会发生内存泄漏。因为,在实际情况中 ThreadLocal 对象不被使用时,代码中几乎不会再调用 set()
、get()
或者 remove()
方法,所以不容易触发清理失效 Entry 流程,从而造成内存泄漏。
除此之外,还有一些其它导致内存泄漏的原因,如下:
- 在 Web 应用中,每个请求通常会由一个线程处理。如果在请求处理过程中使用了 ThreadLocal,并且没有在请求结束时清理它,那么在高并发的情况下,大量的 ThreadLocal 变量副本会被创建并一直存在,导致内存泄漏。
- 很多时候我们都会使用线程池区创建线程执行任务,在使用线程池时,如果在线程执行完毕后没有手动清理 ThreadLocal 变量,那么这些变量的副本将一直存在于线程池中,可能会被其他线程复用,导致数据错乱和内存泄漏。
- 在大多数情况下使用 ThreadLocal 时都将其定义为被 static 修饰的静态变量,这样就相当于 ThreadLocal 存在了一个静态的强引用,导致系统 GC 时无法清理 ThreadLocalMap 中 Entry 的弱引用 key,致使 Entry 无法被清理,从而造成内存泄漏。
10.3 ThreadLocal 使用过程中如何解决内存泄漏问题?
在使用完 ThreadLocal 后要及时的调用 remove()
方法,手动将线程的 ThreadLocal 从 ThreadLocalMap 中删除。
10.4 ThreadLocal 变量为什么使用 static 修饰?
如果 ThreadLocal 变量没有被 static 修饰,定义为成员变量 (而不是静态变量),这样的话每当一个线程创建一个新的当前 ThreadLocal 变量所在类的实例时,就会重复创建一个 ThreadLocal 实例,从而导致不必要的内存开销。
而且 ThreadLocal 变量的作用范围是整个线程,它需要在多个线程之间保持一致,而不是每个线程都创建自己的 ThreadLocal 对象。所以,如果使用 static 修饰 ThreadLocal 对象,将其定义为一个静态变量,这样就能实现被多个线程访问和共享。
10.5 ThreadLocal 如果使用 static 修饰会一直存在一个强引用嘛?
ThreadLocal 对象如果使用 static 修饰,它将存在一个静态的强引用。无论线程结束与否,ThreadLocal 对象都不会被垃圾回收。
当 ThreadLocal 对象被声明为静态变量时,它会被类的静态字段所引用,因此它的生命周期与类的生命周期相同。即使线程执行完毕或者 ThreadLocalMap 中的 Entry 被清理,由于 ThreadLocal 对象存在静态引用,它仍然不会被垃圾回收。
所以,如果在使用 ThreadLocal 时,将其声明为静态变量,需要特别注意在适当的时机调用 remove()
方法或将其设置为 null,以避免潜在的内存泄漏问题。这样可以确保 ThreadLocal 对象在不再需要时能够被垃圾回收。
10.6 ThreadLocalMap 中的 Key 为什么设置为弱引用?
当线程使用 ThreadLocal 实例结束后,就不会再引用 ThreadLocal 实例,但是 ThreadLocalMap.Entry 中的 key 还是会存在与 ThreadLocal 实例的引用。
- 如果将 key 设置为强引用,即使经过 GC 后 ThreadLocalMap 中的 Entry.key 还是存在和 ThreadLocal 实例的引用关系,造成 Entry 对象无法被回收,导致内存泄漏。
- 如果将 key 设置为弱引用,则经过 GC 后 ThreadLocalMap 中的 Entry.key 会断开与 ThreadLocal 实例的引用关系,从而使的 key=null,使 Entry 对象可以被回收。
因此,将 ThreadLocalMap 中的 Key 设置为弱引用,可以使得 ThreadLocal 实例在不被使用时,可以被垃圾收集器回收,从而避免内存泄漏问题。
10.7 ThreadLocalMap 中的 Value 为什么不跟 key 一样设置为弱引用?
在 ThreadLocalMap 中没有将 value 不设置为弱引用,主要是因为如果将 value 设置为弱引用,那么在经过 GC 后,value 可能会被回收掉了,而 key 没有被回收,就造成了 key 还在但是 value 没了的现象,从而造成数据丢失。
--- END ---
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。