最全ThreadLocal总结:InheritableThreadLocal、FastThreadLocal原理应用及高频面试题

文摘   科技   2024-10-13 20:17   广东  

一、为什么要使用ThreadLocal

JAVA并发编程中,多个线程同时访问和修改共享变量是一个常见的场景。为了解决线程安全问题,一种常见的做法是使用锁机制,如synchronized关键字或Lock接口。然而,加锁的方式可能会带来性能上的损失,因为线程之间需要竞争锁,而且在等待锁的过程中会阻塞线程的执行。

另一种解决方案是使用ThreadLocal。ThreadLocal提供了一种空间换时间的方式来解决线程安全问题。它为每个线程创建了一个独立的存储空间,用于保存线程特有的数据。当多个线程访问同一个ThreadLocal变量时,实际上它们访问的是各自线程本地存储的副本,而不是共享变量本身。因此,每个线程都可以独立地修改自己的副本,而不会影响到其他线程。

使用ThreadLocal的好处在于它避免了线程之间的竞争和阻塞,提高了并发性能。同时,它也简化了编程模型,因为开发者不需要显式地使用锁来保护共享变量的访问。

要注意的是,ThreadLocal并不适用于所有场景。它主要适用于每个线程需要独立保存自己的数据副本的情况。如果多个线程之间需要共享数据并进行协作,那么使用锁或其他同步机制可能更为合适。此外,在使用ThreadLocal时也需要注意内存泄漏和数据污染的问题,需要正确地管理和清理线程本地存储的数据。

二、ThreadLocal核心

ThreadLocal是Java中的一个类,它提供了线程局部(thread-local)变量。这些变量与普通的变量不同,因为每个访问变量的线程都有其自己独立初始化的变量副本。通过ThreadLocal实例,可以隔离并保存每个线程的数据,确保线程之间不会相互干扰,避免因并发访问导致的数据不一致问题。

核心特性

  1. 1. 线程隔离:每个线程对 ThreadLocal 变量的修改对其他线程是不可见的。

  2. 2. 无继承性:子线程不能访问父线程的 ThreadLocal 变量,除非子线程中有显式的设置或复制操作。

  3. 3. 避免同步:由于每个线程都有自己的变量副本,因此不需要同步就可以保证线程安全。

常见方法

  1. 1. get:返回当前线程对应的变量的值。如果当前线程没有对应的值,则返回初始值或 null(如果未设置初始值)。

  2. 2. set:设置当前线程对应的变量的值。

  3. 3. remove:删除当前线程对应的变量。

  4. 4. initialValue():这是一个受保护的方法,用于设置变量的初始值。通常,你可以通过匿名内部类来覆盖这个方法。

使用场景

  1. 1. 数据库连接:在多线程应用中,每个线程可能需要自己的数据库连接。使用 ThreadLocal 可以为每个线程保存其自己的连接。

  2. 2. 会话管理:在 Web 应用中,每个用户的会话数据可以使用 ThreadLocal 存储,从而确保同一用户的多个请求在同一个线程中处理时能够访问到正确的会话数据。

  3. 3. 线程内上下文传递:有时需要在同一个线程的不同方法之间传递一些上下文信息,而不希望使用全局变量或参数传递。这时可以使用 ThreadLocal

注意事项

  1. 1. 内存泄漏:如果线程不再需要使用该变量,但忘记调用 remove() 方法来清理,那么由于 ThreadLocalMap 中的 Entry 的 key 是对 Thread 的弱引用,所以 Thread 被回收后,Entry 的 key 会被置为 null,但 value 不会被回收,从而导致内存泄漏。因此,使用完 ThreadLocal 后,最好调用 remove() 方法来清理。

  2. 2. 线程池中的使用:在线程池中,线程可能会被复用。如果线程之前设置过 ThreadLocal 变量,但在使用后没有清理,那么下一个任务可能会读取到上一个任务设置的值。因此,在线程池中使用 ThreadLocal 时需要特别小心。

  3. 3. 初始化问题:如果不重写 initialValue() 方法,并且在使用前没有调用 set() 方法设置值,那么 get() 方法将返回 null。为了避免这种情况,可以重写 initialValue() 方法来提供一个默认值。

  4. 4. 不适用于全局共享状态:虽然 ThreadLocal 可以在多个线程之间隔离数据,但它不适用于需要在多个线程之间共享和修改的全局状态。对于这种情况,应该使用其他同步机制(如锁或原子变量)。

三、ThreadLocal的工作原理


ThreadLocal的工作原理主要是通过每个线程内部的ThreadLocalMap来实现的。ThreadLocalMapThreadLocal的静态内部类,它实现了类似于Map的键值对存储结构,但是键是弱引用(WeakReference)类型的ThreadLocal对象,而值则是与线程相关的数据。

每个线程都有一个名为threadLocals的成员变量,这个变量就是ThreadLocalMap类型的。当线程调用ThreadLocalset()方法时,它会将ThreadLocal对象和要存储的值作为键值对添加到自己的threadLocals中。当调用get()方法时,线程会从自己的threadLocals中根据ThreadLocal对象查找对应的值。

由于每个线程都有自己的threadLocals,因此它们之间不会共享这些线程局部变量的值。这就是ThreadLocal能够实现线程隔离的原因。

四、ThreadLocal的用法

使用ThreadLocal非常简单,只需要按照以下步骤即可:

  1. 1. 创建一个ThreadLocal对象:ThreadLocal<T> threadLocal = new ThreadLocal<>();

  2. 2. 在需要设置线程局部变量的地方调用set()方法:threadLocal.set(value);

  3. 3. 在需要获取线程局部变量的地方调用get()方法:T value = threadLocal.get();

  4. 4. 在不再需要线程局部变量时,调用remove()方法清理资源:threadLocal.remove();

由于ThreadLocal中的值是与线程相关的,因此在使用完ThreadLocal后,最好及时调用remove()方法清理资源,以避免潜在的内存泄漏问题。

ThreadLocal跟踪每个线程处理的任务数量

先定义一个TaskCounter类,该类使用ThreadLocal来存储每个线程的任务计数器:

public class TaskCounter {
    // 使用ThreadLocal来存储每个线程的任务计数器
    private static final ThreadLocal<Integer> taskCountThreadLocal = ThreadLocal.withInitial(() -> 0);

    /**
     * 增加当前线程的任务计数器
     */

    public static void increment() {
        taskCountThreadLocal.set(taskCountThreadLocal.get() + 1);
    }

    /**
     * 获取当前线程的任务计数器值
     *
     * @return 当前线程的任务计数器值
     */

    public static int getCount() {
        return taskCountThreadLocal.get();
    }

    /**
     * 重置当前线程的任务计数器
     */

    public static void reset() {
        taskCountThreadLocal.set(0);
    }
}

然后,创建一个WorkerThread类,模拟线程执行任务并更新任务计数器:

public class WorkerThread extends Thread {
    private final int taskId;

    public WorkerThread(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println("Thread " + Thread.currentThread().getId() + " is starting task " + taskId);
        
        // 开始任务前,增加任务计数器
        TaskCounter.increment();
        
        // 模拟任务执行时间
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 任务完成后,打印任务计数器
        System.out.println("Thread " + Thread.currentThread().getId() + " completed task " + taskId + ". Total tasks: " + TaskCounter.getCount());
        
        // 可以选择在此处重置任务计数器,或者在其他适当的时候重置
        // TaskCounter.reset();
    }
}

最后,在主程序中创建并启动多个工作线程,并观察每个线程的任务计数器:

public class Main {
    public static void main(String[] args) {
        // 创建并启动5个工作线程
        for (int i = 1; i <= 5; i++) {
            new WorkerThread(i).start();
        }
    }
}

TaskCounter类利用ThreadLocal为每个线程维护了一个独立的任务计数器。WorkerThread类在每次执行任务时,通过调用TaskCounter.increment()来增加当前线程的任务计数器,并通过TaskCounter.getCount()来获取当前计数器的值。

五、ThreadLocal的内存泄漏问题

虽然ThreadLocal可以有效地实现线程隔离,但是它也存在一定的内存泄漏风险。这主要是因为ThreadLocalMap中的键是弱引用类型的ThreadLocal对象。当ThreadLocal对象不再被强引用时,它会被垃圾回收器回收,但是对应的键值对仍然保留在ThreadLocalMap中。如果线程长时间运行且没有调用remove()方法清理资源,那么这些无用的键值对会占用内存空间,从而导致内存泄漏。

为了避免这个问题:

  1. 1. 在使用完ThreadLocal后,及时调用remove()方法清理资源。

  2. 2. 使用静态内部类来持有ThreadLocal对象,以确保它不会被提前回收。

  3. 3. 尽量避免在长时间运行的线程中使用ThreadLocal

  4. 4. 使用Java 8引入的InheritableThreadLocal来替代ThreadLocal,它可以在子线程中自动继承父线程的线程局部变量值,从而避免在创建新线程时重复设置值的问题。但是同样需要注意及时清理资源以避免内存泄漏。

六、源码分析

ThreadLocal 是 Java 中用于创建线程局部变量的类。线程局部变量是每个线程都有自己独立实例的变量,与其他线程的实例相互隔离。

主要成员变量

  • • ThreadLocalMap threadLocals:这是 Thread 类中的一个字段,用于存储线程局部变量的映射。它不是 ThreadLocal 类的直接成员,但它是实现线程隔离的关键。

  • • ThreadLocalMap inheritableThreadLocals:同样在 Thread 类中,用于存储可继承的线程局部变量。

在 ThreadLocal 类内部,没有直接引用这些字段,而是通过静态方法访问当前线程的 threadLocals 字段。

ThreadLocal 本身并不直接存储数据,而是作为一个工具类,提供了访问和操作线程局部变量的方法。实际上,数据的存储是由 ThreadLocal 的内部类 ThreadLocalMap 来完成的。每个 Thread 对象都含有一个名为 threadLocals 的 ThreadLocalMap 类型的成员变量,这个变量用于存储当前线程中所有 ThreadLocal 对象的值。

这里简要概括一下 ThreadLocalThreadLocalMap 和 Thread 之间的关系:

  1. 1. ThreadLocal:这是一个工具类,提供了 set(T value)get() 和 remove() 等方法来操作线程局部变量。但是,它本身不直接存储数据。

  2. 2. ThreadLocalMap:这是 ThreadLocal 的一个静态内部类,实际上是一个定制化的哈希表(但不是 java.util.HashMap)。它用于存储线程局部变量的值,并且每个线程都有一个这样的映射。这个映射的键是 ThreadLocal 对象,值是对应的线程局部变量的值。

  3. 3. Thread:Java 中的线程类。每个 Thread 对象都有一个 threadLocals 字段,这是一个 ThreadLocalMap 实例,用于存储该线程中所有 ThreadLocal 变量的当前值。当线程调用 ThreadLocal 的 set 方法时,它实际上是在自己的 threadLocals 映射中设置值;当调用 get 方法时,它是从自己的 threadLocals 映射中检索值。

这种设计使得每个线程都可以独立地管理自己的 ThreadLocal 变量,而不会与其他线程的变量发生冲突。这是多线程编程中一个非常有用的特性,因为它允许开发者在不使用显式锁的情况下维护线程安全的状态。

核心方法

  1. 1. get()

   public T get() {
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);
       if (map != null) {
           ThreadLocalMap.Entry e = map.getEntry(this);
           if (e != null) {
               @SuppressWarnings("unchecked")
               T result = (T)e.value;
               return result;
           }
       }
       return setInitialValue();
   }

get()方法首先获取当前线程,然后尝试从线程的threadLocals字段中获取ThreadLocalMap。如果映射存在且包含当前 ThreadLocal实例的条目,则返回对应的值。否则,调用setInitialValue()` 来设置初始值。

  1. 2. set(T value)

   public void set(T value) {
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);
       if (map != null)
           map.set(this, value);
       else
           createMap(t, value);
   }

set() 方法将给定的值设置到当前线程的 threadLocals 字段中,对应于当前 ThreadLocal 实例的键。如果映射不存在,则创建一个新的映射。

  1. 3. remove()

   public void remove() {
       ThreadLocalMap m = getMap(Thread.currentThread());
       if (m != null)
           m.remove(this);
   }

remove() 方法从当前线程的 threadLocals 字段中移除当前 ThreadLocal 实例对应的条目。

  1. 4. initialValue()

   protected T initialValue() {
       return null;
   }

initialValue()方法是ThreadLocal的一个受保护方法,用于返回变量的初始值。默认情况下,它返回null`,但子类可以覆盖此方法以提供自定义的初始值。

辅助方法

  • • **getMap(Thread t)**:获取线程 t 的 threadLocals 映射。

  • • **createMap(Thread t, T firstValue)**:在线程 t 中创建一个新的 ThreadLocalMap,并将给定的值设置到映射中,对应于当前 ThreadLocal 实例的键。

  • • **setInitialValue()**:这个方法首先调用 initialValue() 来获取初始值,然后调用 set() 方法将这个初始值设置到当前线程的映射中。这是一个延迟初始化的策略。

ThreadLocalMap

ThreadLocalMap 是一个自定义的哈希表实现,专门用于存储线程局部变量。它的键是 ThreadLocal 对象,值是对应的线程局部变量的值。这个映射的实现非常特殊,它使用了开放寻址法和线性探测来解决哈希冲突。

每个 ThreadLocalMap 实例都是与特定线程关联的,因此它不需要考虑同步。这也意味着不同的线程之间不会有对映射的并发访问,从而实现了线程隔离。

七、ThreadLocal扩展之InheritableThreadLocal

InheritableThreadLocal 是 ThreadLocal 的扩展。与 ThreadLocal 每个线程持有其自己的独立值副本不同,InheritableThreadLocal 有一个特性,那就是当创建一个新的线程时,如果父线程中有一个 InheritableThreadLocal 变量,那么子线程将会继承这个变量的值。这意味着子线程可以访问其父线程为此类变量设置的值。

这个特性在某些场景下非常有用,比如当你希望在整个线程树中共享某些数据时,但又不希望这些数据被其他无关的线程所访问。

用法

public class InheritableThreadLocalExample {
    
    // 创建一个 InheritableThreadLocal 变量
    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        // 在主线程中设置值
        inheritableThreadLocal.set("这是父线程的值");
        
        System.out.println("父线程中的值: " + inheritableThreadLocal.get());
        
        // 创建一个子线程
        Thread childThread = new Thread(() -> {
            // 在子线程中尝试获取值,由于使用了 InheritableThreadLocal,这里会获取到父线程中设置的值
            System.out.println("子线程中的值: " + inheritableThreadLocal.get());
        });
        
        // 启动子线程
        childThread.start();
        
        // 等待子线程执行完成
        try {
            childThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 主线程结束时清除值,防止潜在的内存泄漏
        inheritableThreadLocal.remove();
    }}

八、ThreadLocal优化之FastThreadLocal

FastThreadLocal 是 Netty 框架提供的一个高性能的线程局部变量实现,它旨在提供比 Java 标准库中的 ThreadLocal 更快的访问速度。

FastThreadLocal 通过使用内部数组和变量索引技术减少了访问线程局部变量的时间,提高了性能。这种实现特别适合在高频率访问线程局部变量的场景中使用。

FastThreadLocal 的主要优势在于其高效的内存访问模式和减少了间接引用,这有助于减少缓存未命中的情况,并提高内存访问的局部性。然而,需要注意的是,FastThreadLocal 主要是为 Netty 内部使用而设计的,但也可以在普通的 Java 应用中使用。

使用:

import io.netty.util.concurrent.FastThreadLocal;
import io.netty.util.concurrent.FastThreadLocalThread;

public class FastThreadLocalExample {

    // 创建一个 FastThreadLocal 变量
    private static final FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<>();

    public static void main(String[] args) {
        // 由于 FastThreadLocal 是为 Netty 设计的,它通常与 Netty 的线程模型一起使用。
        // 但为了演示,我们来创建一个 FastThreadLocalThread 来模拟 Netty 的线程。
        FastThreadLocalThread thread = new FastThreadLocalThread(() -> {
            // 在线程中设置值
            fastThreadLocal.set("FastThreadLocal 中的值");

            // 获取并打印值
            System.out.println("线程中的值: " + fastThreadLocal.get());

            // 清除值,防止内存泄漏
            fastThreadLocal.remove();
        });

        // 启动线程
        thread.start();
    }
}

如果在非 Netty 线程(即非 FastThreadLocalThread)中使用 FastThreadLocal,你需要手动调用 FastThreadLocal.initialize(Thread) 方法来初始化线程的局部变量存储。但是,在 Netty 的环境中,这一切都是自动处理的。

九、 ThreadLocal相关的面试题

面试题1:请解释一下ThreadLocal是什么以及它的用途?

答案: ThreadLocal是Java中的一个类,它提供了线程局部(thread-local)变量。这些变量与普通的变量不同,因为每个访问变量的线程都有其自己独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,它们用于保存属于线程特有的状态,如用户ID、事务ID等。通过使用ThreadLocal,可以避免在多线程环境中使用同步,从而提高程序性能。

面试题2:ThreadLocal是如何实现线程局部存储的?

答案: ThreadLocal内部使用了一个称为ThreadLocalMap的自定义哈希映射,来存储线程局部变量。每个Thread对象都有一个与之关联的ThreadLocalMap,这个映射将ThreadLocal对象作为键,将线程局部变量的值作为值。当线程调用ThreadLocalset方法时,它会在自己的ThreadLocalMap中存储一个键值对;调用get方法时,它会从自己的映射中检索值。由于每个线程都有自己的ThreadLocalMap,因此它们可以独立地存储和检索值,而不会与其他线程冲突。

面试题3:ThreadLocal可能会导致哪些问题?

答案: ThreadLocal使用不当可能会导致内存泄漏和数据污染问题。

  • • 内存泄漏:如果线程不再需要,但线程池将其重用,并且之前的线程设置了ThreadLocal变量但没有清除,那么这些变量可能会占用内存而无法被垃圾收集器回收。这可以通过在不再需要ThreadLocal变量时调用其remove方法来避免。

  • • 数据污染:当线程被线程池重用时,如果之前的任务没有清除其设置的ThreadLocal变量,那么新任务可能会意外地访问到这些旧数据。为了避免这种情况,应该在每个任务开始时清除可能存在的ThreadLocal变量。

面试题4:ThreadLocal与synchronized有何不同?

答案: ThreadLocalsynchronized都是用于处理多线程编程中共享资源访问问题的技术,但它们的工作原理和应用场景不同。

  • • ThreadLocal:它提供了线程局部变量,每个线程都有其自己的变量副本。这样,线程可以独立地操作自己的数据,而不需要与其他线程同步。ThreadLocal适用于每个线程需要独立保存自己状态的情况。

  • • synchronized:它是一种内置的同步机制,用于控制多个线程对共享资源的访问。通过使用synchronized关键字,可以确保一次只有一个线程能够执行某个代码块或方法,从而避免线程安全问题。synchronized适用于多个线程需要共享和协作访问同一资源的情况。

面试题5:ThreadLocal为什么会导致内存泄漏?

答案

ThreadLocal导致内存泄漏的主要原因在于其内部类ThreadLocalMap中的键值对可能不会被垃圾收集器正确回收。ThreadLocalMapThread类的一个成员变量,用于存储每个线程自己的ThreadLocal变量副本。

每个ThreadLocal实例在ThreadLocalMap中作为键存在,与之关联的值是线程特有的数据。当线程不再需要这些数据,并且没有显式地调用ThreadLocalremove()方法来清除它们时,这些键值对仍然保留在ThreadLocalMap中。

如果线程是长时间运行的(比如线程池中的线程),那么这些未清除的键值对将长时间占用内存。更糟糕的是,如果ThreadLocal实例本身是一个匿名内部类或者静态类的实例,并且持有了外部类的引用,那么外部类实例也可能无法被垃圾收集,从而导致更严重的内存泄漏。

此外,即使线程最终终止,Thread对象本身(以及它的ThreadLocalMap)可能也不会立即被垃圾收集,特别是在使用了线程池的情况下。因此,长时间不清理的ThreadLocal变量可能导致应用程序的可用内存逐渐减少,最终导致OutOfMemoryError

为了避免这种内存泄漏,最佳实践是在不再需要ThreadLocal变量时显式调用其remove()方法。这确保了与当前线程关联的ThreadLocalMap中的相应条目被正确删除,从而允许垃圾收集器回收相关内存。在使用线程池时尤其重要,因为线程可能会被重用,而它们的ThreadLocalMap也会随之保留。

面试题6:为什么ThreadLocal的key要用弱引用?

答案

ThreadLocal的key使用弱引用的主要目的是为了帮助避免内存泄漏。在Java中,弱引用(WeakReference)是一种引用类型,它不会阻止其引用的对象被垃圾收集器回收。当垃圾收集器运行时,如果发现一个对象仅被弱引用所引用,那么它就会回收该对象。

ThreadLocalMap中,key是ThreadLocal对象,value是与线程相关的值。如果ThreadLocal的key使用强引用,那么只要线程对象存在(比如线程池中的线程),即使ThreadLocal实例在其他地方已经没有被引用,它也不会被垃圾收集器回收,因为ThreadLocalMap中还持有对它的强引用。这种情况下,如果ThreadLocal对象持有了其他资源(如大对象、数据库连接等),那么这些资源也不会被回收,从而导致内存泄漏。

通过使用弱引用作为ThreadLocalMap中的key,当ThreadLocal实例在其他地方不再被引用时,垃圾收集器可以回收它。这样,即使线程仍然存在,与之关联的ThreadLocal对象也可以被清理,从而释放了它所持有的资源。然而,需要注意的是,仅仅将key设置为弱引用并不足以完全避免内存泄漏。如果value本身持有了其他不应该被泄漏的资源,那么这些资源仍然可能被泄漏。因此,正确使用ThreadLocal(包括在不再需要时调用remove()方法)仍然是避免内存泄漏的关键。

太强 ! SpringBoot中出入参增强的5种方法 : 加解密、脱敏、格式转换、时间时区处理

太强 ! SpringBoot中优化if-else语句的七种绝佳方法实战

SpringBoot使用EasyExcel并行导出多个excel文件并压缩zip下载
提升编程效率的利器: Google Guava库中双向映射BitMap
从MySQL行格式原理看:为什么开发规范中不推荐NULL?数据是如何在磁盘上存储的?
SpringBoot中使用Jackson实现自定义序列化和反序列化控制的5种方式总结

提升编程效率的利器: Google Guava库之RateLimiter优雅限流

深入JVM逃逸分析原理:且看其如何提高程序性能和内存利用率

必知必会!MySQL索引下推:原理与实战

深入解析JVM内存分配优化技术:TLAB

SpringBoot中基于JWT的双token(access_token+refresh_token)授权和续期方案
SpringBoot中基于JWT的单token授权和续期方案
SpringBoot中Token登录授权、续期和主动终止的方案(Redis+Token)
微服务中token鉴权设计的4种方式总结
提升编程效率的API利器:精通Google Guava库区间范围映射RangeMap
SpringBoot中Jackson控制序列化和反序列化的注解和扩展点总结【收藏版】

SpringBoot中大量数据导出方案:使用EasyExcel并行导出多个excel文件并压缩zip后下载

提升编程效率的API利器:精通Google Guava库之IO工具类
提升编程效率的API利器:精通Google Guava库二维映射表Table
提升编程效率的API利器:精通Google Guava库区间范围映射RangeMap
提升编程效率的利器: Google Guava库中双向映射BitMap
提升编程效率的利器: Google Guava库之RateLimiter优雅限流
基于Guava布隆过滤器的海量字符串高效去重实践

关注『 码到三十五 』,日有所获
                     点赞 和 在看 就是最大的支持

码到三十五
主要分享正经的开发技术(原理,架构,实践,源码等),以输出驱动输入;当然偶尔会穿插点生活琐碎,顺便吃个瓜,目的嘛,搞点精准流量,看能不能发发广告。
 最新文章