写点什么

ThreadLocal

作者:Yaien
  • 2023-12-18
    广西
  • 本文字数:4812 字

    阅读完需:约 16 分钟

ThreadLocal

ThreadLocal 是 Java 中的一个线程局部变量,它允许每个线程独立地存储和获取数据,保证线程之间的数据互相独立,避免并发访问带来的竞争条件。


ThreadLocal 不是用来解决共享数据的问题,而是为了实现线程隔离的目的。它在某些场景下非常有用,如 Web 应用中的用户身份信息、数据库连接、事务管理等。

使用

public class UserContext {    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
public static void setUser(User user) { userThreadLocal.set(user); }
public static User getUser() { return userThreadLocal.get(); }
public static void clearUser() { userThreadLocal.remove(); }}
// 在某个方法中设置用户身份信息User user = // 获取用户身份信息UserContext.setUser(user);
// 在其他方法中获取用户身份信息User currentUser = UserContext.getUser();
复制代码


在上面的示例中,通过 UserContext 类和 userThreadLocal 实例,我们可以在不同的方法中共享用户身份信息,无需显式传递。可以通过 UserContext.setUser(user)方法设置用户信息,然后在其他方法中使用 UserContext.getUser()获取该用户信息。最后,可以通过 UserContext.clearUser()方法清除当前线程的用户信息。

作用

  1. 提供线程封闭(Thread Confinement)机制:ThreadLocal 允许每个线程拥有自己的数据副本,避免了多线程之间的数据竞争问题。每个线程都可以独立地修改和访问自己的数据副本,而不会影响其他线程。

  2. 实现线程安全:通过将数据保存在 ThreadLocal 中,可以在多线程环境下实现线程安全。每个线程访问自己的 ThreadLocal 实例,不会受到其他线程的干扰,避免了使用锁机制的开销和复杂性。

  3. 提供线程间上下文传递:ThreadLocal 可以用于在线程之间传递上下文信息,而不需要显式传递参数。在某些场景下,需要在多个方法或组件之间共享数据,但又不希望公开全局变量或传递参数,这时可以使用 ThreadLocal 来存储和获取数据。

  4. 解决线程安全问题:在某些情况下,一些对象可能不是线程安全的,无法在多线程环境下直接使用。通过将非线程安全的对象存储在 ThreadLocal 中,每个线程都拥有自己的副本,从而避免了并发访问的问题。

使用场景

  1. Web 应用中的用户身份信息:可以在请求处理链路的各个组件中共享用户身份信息,无需在每个方法中显式传递。

  2. 数据库连接、事务管理:ThreadLocal 可以用于存储数据库连接、事务对象等,使每个线程都拥有独立的连接或事务,从而避免并发访问的问题。

  3. 线程池中的任务隔离:使用 ThreadLocal 可以为每个任务提供独立的上下文环境,避免数据混乱。

方法

ThreadLocal 类接口很简单,其提供了一下方法:


  1. get(): 用于获取当前线程的 ThreadLocal 实例中存储的值。如果该线程尚未设置过值,则会使用初始值提供者(如果已设置)或返回 null。

  2. set(T value): 将指定的值设置到当前线程的 ThreadLocal 实例中。

  3. remove(): 从当前线程的 ThreadLocal 实例中移除值。这样做可以避免潜在的内存泄漏,因为 ThreadLocal 会持有线程的引用。

  4. initialValue(): 该方法在首次调用 get()方法时被调用,返回 ThreadLocal 的初始值。默认情况下返回 null,可以通过继承 ThreadLocal 并重写该方法来自定义初始值。

  5. setInitialValue(ThreadLocal<?> local, Object value): 设置指定 ThreadLocal 实例的初始值。内部调用了 initialValue()方法来获取初始值,并将其设置到指定的 ThreadLocal 实例中。

  6. createMap(ThreadLocal<?> firstKey, Object firstValue): 在首次调用 set()方法时被调用,用于创建 ThreadLocal 实例的内部映射。

实现

ThreadLocal 的底层实现主要依赖于 Thread 类中的一个成员变量 ThreadLocalMap。


每个 Thread 对象中都有一个 ThreadLocalMap 对象,用于存储线程的 ThreadLocal 实例以及对应的值。ThreadLocalMap 是一个自定义的哈希表结构,用于实现线程局部变量的存储和访问。


在 ThreadLocal 类中,使用 ThreadLocalMap 来管理每个线程的 ThreadLocal 实例及其对应的值。每个 ThreadLocal 实例作为 ThreadLocalMap 的键,而实际存储的值则作为 ThreadLocalMap 的值。


当调用 ThreadLocal 的 set()方法时,实际上是通过 Thread 对象获取当前线程的 ThreadLocalMap,并将 ThreadLocal 实例作为键,要设置的值作为值,存储到 ThreadLocalMap 中。同样,当调用 ThreadLocal 的 get()方法时,也是通过 Thread 对象获取当前线程的 ThreadLocalMap,并根据 ThreadLocal 实例获取对应的值。


为了保证高效的查找和存储,ThreadLocalMap 使用线性探测法来解决哈希冲突。在哈希冲突的情况下,使用开放地址法寻找下一个可用的槽位。


需要注意的是,由于 ThreadLocalMap 是作为 Thread 的成员变量存在的,它是与每个线程绑定的。因此,每个线程都拥有自己的 ThreadLocalMap 和其中的数据,相互之间不会产生干扰。


另外,由于 ThreadLocal 在使用完毕后需要进行及时的清理,避免内存泄漏。Java 中的 ThreadLocal 实现中并没有提供自动清理的机制,因此需要手动调用 remove()方法或使用 try-finally 块来确保在使用完 ThreadLocal 后进行清理操作。


总之,ThreadLocal 通过 Thread 对象中的 ThreadLocalMap 来实现线程局部变量的存储和访问,保证了每个线程拥有自己独立的数据副本,避免了并发访问的竞争条件。

开放地址法

开放地址法(Open Addressing)是一种用于解决哈希冲突的方法,常用于哈希表的实现。当多个键映射到相同的哈希桶位置时,开放地址法使用一定的规则来寻找下一个可用的桶位,以存储冲突的键。


在开放地址法中,哈希表通常是一个固定大小的数组,称为哈希桶(Hash Bucket)。每个桶可以存储一个键值对或者被标记为空。当发生哈希冲突时,即多个键映射到同一个桶位置时,开放地址法会根据一定的规则去寻找下一个可用的桶位置,直到找到一个空桶或者遍历整个哈希表。


常见的开放地址法策略有以下几种:


  1. 线性探测(Linear Probing):当发生哈希冲突时,线性探测会逐个查找下一个桶,直到找到一个空桶或者遍历整个哈希表。具体地,下一个桶的位置计算公式为 index = (index + 1) % capacity,其中 index 是当前桶的位置,capacity 是哈希表的容量。

  2. 二次探测(Quadratic Probing):二次探测通过使用二次方程来计算下一个桶的位置。具体地,下一个桶的位置计算公式为 index = (index + i * i) % capacity,其中 index 是当前桶的位置,i 是探测次数(从 1 开始),capacity 是哈希表的容量。

  3. 双重散列(Double Hashing):双重散列使用两个不同的哈希函数来计算下一个桶的位置。具体地,下一个桶的位置计算公式为 index = (index + i * hash2(key)) % capacity,其中 index 是当前桶的位置,i 是探测次数(从 1 开始),hash2(key)是第二个哈希函数的计算结果,capacity 是哈希表的容量。


在使用开放地址法解决哈希冲突时,需要注意以下几点:


  1. 负载因子控制:为了保持较低的哈希冲突率,需要合理选择哈希表的容量,以及根据实际情况调整负载因子。负载因子是哈希表中已存储元素数量与桶位总数的比值,当负载因子较高时,哈希冲突的可能性增加,需要进行扩容操作。

  2. 删除元素的处理:在使用开放地址法时,删除元素可能会导致后续查询操作无法正确定位到原有的元素。

内存泄露问题

使用 ThreadLocal 时,需要注意潜在的内存泄漏问题。以下是一些可能引起内存泄漏的情况以及如何避免它们:


  1. 未及时调用 remove()方法:在使用完 ThreadLocal 后,应该及时调用 remove()方法清除当前线程的 ThreadLocal 实例中存储的数据。如果没有显式调用 remove()方法,ThreadLocal 实例可能会一直持有对线程的引用,导致线程无法被垃圾回收,从而造成内存泄漏。因此,在使用完 ThreadLocal 后,应该确保在适当的时机调用 remove()方法,例如在线程执行结束或不再需要 ThreadLocal 存储的数据时。

  2. 长时间存储大量数据:如果在 ThreadLocal 中存储大量的数据,并且线程的生命周期很长,可能会导致内存占用过高。因为 ThreadLocal 的数据是与线程绑定的,线程的长时间存活会导致 ThreadLocal 中的数据一直存在内存中。在这种情况下,可以考虑合理控制数据的大小或使用完后及时清理数据,避免过多的内存占用。

  3. 线程池中未清理 ThreadLocal 数据:在使用线程池时,如果使用了 ThreadLocal,需要特别注意清理 ThreadLocal 数据。因为线程池的线程会被重用,ThreadLocal 中的数据可能会被下一个任务错误地共享。为了避免这种情况,可以在任务执行结束后显式调用 remove()方法清理 ThreadLocal 数据,或使用线程池提供的钩子方法(如 afterExecute())来清理 ThreadLocal 数据。

  4. 应用服务器的线程池问题:在使用应用服务器(如 Tomcat)时,如果将 ThreadLocal 放在静态变量中,并且使用了应用服务器的线程池,可能会引发内存泄漏。因为应用服务器的线程池线程是被重用的,ThreadLocal 中的数据可能会在不同请求之间共享。为了避免这种情况,应尽量避免将 ThreadLocal 放在静态变量中,或者在使用完后及时清理 ThreadLocal 数据。


public class ThreadLocalMemoryLeak {    private static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException { ThreadLocalMemoryLeak demo = new ThreadLocalMemoryLeak(); demo.startThread();
// 等待一段时间,模拟业务处理过程 Thread.sleep(2000);
// 主线程结束后,由于没有调用remove()方法清理ThreadLocal数据,可能会导致内存泄漏 System.out.println("Main thread finished."); }
private void startThread() { Thread thread = new Thread(() -> { // 在子线程中设置大量数据到ThreadLocal中 byte[] data = new byte[1024 * 1024 * 10]; // 10MB threadLocal.set(data);
try { // 模拟子线程的业务逻辑 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
// 不调用remove()方法,可能会导致内存泄漏 });
thread.start(); }}
复制代码


在上述示例中,我们创建了一个 ThreadLocal 实例,并在子线程中将大量的数据存储到 ThreadLocal 中。然而,在子线程结束后,我们没有调用 remove()方法来清理 ThreadLocal 中的数据。 如果运行该示例,主线程结束后,由于没有清理 ThreadLocal 中的数据,可能会导致内存泄漏。每个线程的 ThreadLocal 实例会持有对线程的引用,导致线程无法被垃圾回收。 为了避免内存泄漏,应该在合适的时机调用 remove()方法,清理 ThreadLocal 中的数据。在上述示例中,可以在子线程的末尾添加 threadLocal.remove()来手动清理 ThreadLocal 数据。


为了避免 ThreadLocal 可能引发的内存泄漏问题,可以采取以下措施:


  1. 及时调用 remove()方法:在使用完 ThreadLocal 后,应该在合适的时机调用 remove()方法,清理 ThreadLocal 中的数据。可以使用 try-finally 块,确保在使用完后无论是否发生异常都能够调用 remove()方法。

  2. 使用线程池时注意清理:如果在使用线程池时使用了 ThreadLocal,需要特别注意清理 ThreadLocal 数据。在任务执行结束后,可以通过线程池提供的钩子方法(如 afterExecute())来清理 ThreadLocal 数据。

  3. 使用 InheritableThreadLocal 时小心传递:如果使用 InheritableThreadLocal,它会将数据从父线程传递给子线程。在使用 InheritableThreadLocal 时,需要注意在子线程中是否需要清理或重置 ThreadLocal 数据,以防止不必要的数据泄漏。

  4. 避免将 ThreadLocal 放在静态变量中:在某些情况下,将 ThreadLocal 实例放在静态变量中可能导致意外的内存泄漏。尽量避免在静态变量中使用 ThreadLocal,或者在使用完后及时清理数据。

  5. 使用弱引用的 ThreadLocal 实现:可以自定义 ThreadLocal 的子类,使用弱引用(WeakReference)来持有线程本地的值。这样,在没有其他强引用指向 ThreadLocal 实例时,ThreadLocal 的键值对将被垃圾回收。

发布于: 刚刚阅读数: 6
用户头像

Yaien

关注

还未添加个人签名 2023-12-15 加入

还未添加个人简介

评论

发布
暂无评论
ThreadLocal_Java_Yaien_InfoQ写作社区