Java ThreadLocal使用场景案例详解,深度原理解析

Java技术 潘老师 9个月前 (08-17) 209 ℃ (0) 扫码查看

一、ThreadLocal概述

ThreadLocal被称为线程局部变量,意味着其中存储的变量属于特定线程,对其他线程而言是隔离的。换句话说,ThreadLocal中的变量是每个线程拥有的独立副本,使得每个线程可以访问自己独立的变量副本。

二、ThreadLocal的特性

ThreadLocal变量即线程局部变量,同一个ThreadLocal所持有的对象,在不同Thread中存在不同的副本。以下几点需要予以关注:

  1. 每个Thread都拥有自己的实例副本,这种设计反映在ThreadLocal名称中。
  2. 由于每个Thread都拥有自己的实例副本,其它Thread无法直接访问,因此避免了多线程间的共享问题。
  3. ThreadLocal提供了线程本地的实例,区别于普通变量。每个使用该变量的Thread都会初始化独立的实例副本。通常,ThreadLocal变量会被标记为private static。当Thread结束时,其所使用的所有ThreadLocal实例副本都会被垃圾回收。

下图可以帮助理解:

三、ThreadLocal的应用场景

总体来说,ThreadLocal适用于以下情况:

  • 在每个线程中需要独立实例的场景,且这些实例需要在多个方法中共享。
  • ThreadLocal的使用有助于在线程间实现隔离,同时在方法或类之间实现共享。

四、ThreadLocal的用途

在多线程应用程序中,有时候需要在线程之间共享一些数据,但又不希望使用全局变量或者传递参数的方式来实现。这时,ThreadLocal就能发挥作用。以下是一些ThreadLocal常见的用途:

  1. 线程上下文传递: 在某些情况下,需要在线程之间传递一些上下文信息,比如用户身份、语言环境等。使用ThreadLocal可以方便地将这些信息与线程绑定,避免在方法参数中传递。
  2. 数据库连接管理: 在多线程的数据库访问场景中,每个线程都可以拥有自己的数据库连接,使用ThreadLocal可以确保每个线程独立地管理自己的数据库连接。
  3. 日期格式化: SimpleDateFormat等日期格式化类不是线程安全的,但可以使用ThreadLocal为每个线程创建独立的日期格式化实例,避免线程安全问题。
  4. 事务管理: 在一些需要手动管理事务的环境中,可以使用ThreadLocal来存储当前线程的事务状态。

五、ThreadLocal的使用案例

当在Java多线程环境中需要为每个线程存储特定的用户身份信息时,可以使用 ThreadLocal 类。ThreadLocal 允许在每个线程中存储独立的数据副本,以保证线程之间不会相互干扰。以下是一个使用 ThreadLocal 来存储用户身份信息的简单示例:

public class UserContext {
    private static ThreadLocal<String> userThreadLocal = new ThreadLocal<>();

    public static void setUser(String user) {
        userThreadLocal.set(user);
    }

    public static String getUser() {
        return userThreadLocal.get();
    }

    public static void clear() {
        userThreadLocal.remove();
    }
}

public class UserTask implements Runnable {
    private String user;

    public UserTask(String user) {
        this.user = user;
    }

    @Override
    public void run() {
        UserContext.setUser(user);
        System.out.println("Thread " + Thread.currentThread().getId() + ": User set to " + user);

        // Simulate some work
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Thread " + Thread.currentThread().getId() + ": Current user is " + UserContext.getUser());

        UserContext.clear();
        System.out.println("Thread " + Thread.currentThread().getId() + ": User context cleared");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(new UserTask("UserA"));
        Thread t2 = new Thread(new UserTask("UserB"));

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

在这个示例中,UserContext 类使用 ThreadLocal 来存储用户身份信息。setUser 方法将用户信息设置到当前线程的 ThreadLocal 副本中,getUser 方法从 ThreadLocal 中获取用户信息,而 clear 方法则用于清除线程的 ThreadLocal 副本。

UserTask 类是一个简单的任务类,它在 run 方法中设置用户信息到 UserContext,模拟一些工作,然后再次获取用户信息并清除 UserContext

Main 类创建两个线程,每个线程运行一个 UserTask 实例,分别为不同的用户设置身份信息。

当运行上述代码示例时,可能会得到类似以下的输出结果(注意,由于多线程的随机性,每次运行结果可能会有所不同):

Thread 11: User set to UserA
Thread 12: User set to UserB
Thread 11: Current user is UserA
Thread 12: Current user is UserB
Thread 11: User context cleared
Thread 12: User context cleared

六、内存泄漏与解决方法

使用 ThreadLocal 时要小心内存泄漏的问题。如果不手动清除 ThreadLocal 中的数据,可能会导致长时间运行的线程持续持有这些数据,从而导致内存泄漏。

为了避免内存泄漏,可以使用以下两种方式之一:

1)在不需要的时候调用 remove() 方法清除数据:

threadLocal.remove();

2)使用 Java 8 引入的 ThreadLocal.withInitial() 方法,在每个线程首次访问时初始化数据:

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Default Value");

七、深入解析ThreadLocal原理

ThreadLocal很容易导致误解,初看之下可能会错误地将其理解为本地线程,然而实际情况并非如此。ThreadLocal实际上并非代表一个线程,而是作为线程的局部变量存在,这一点可以通过其源代码得以明确。

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}    

当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程 提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal 既然是线程局部变量,那么理所当然就应该存储在自己的线程对象中,从源码中可以看到线程局部变量是存储在 Thread 对象的 threadLocals 属性中,而 threadLocals 属性是一个 ThreadLocal.ThreadLocalMap 对象。

1、Thread 、ThreadLocal 、 ThreadLocalMap 之间的关系

Thread 中的 threadLocals 属性就是 ThreadLocal.ThreadLocalMap

ThreadLocalMapThreadLocal 的静态内部类:

public class ThreadLocal {
  
    // 省略部分代码 ...
    
    static class ThreadLocalMap {
        // 静态内部类
        static class Entry extends WeakReference> {
        
            /** The value associated with this ThreadLocal. */
            Object value;

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

        // ThreadLocalMap 的初始容量   
        private static final int INITIAL_CAPACITY = 16;
        
        private Entry[] table;
        
        private int size = 0;
   }
   
   // 省略部分代码 ...
}   

通过这段代码可以画出如下关系图:

总结:一个 Thread 中只有一个 ThreadLocalMap ,一个 ThreadLocalMap 中可以有多个ThreadLocal 对象,其中一个 ThreadLocal 对象对应一个 ThreadLocalMap 中的一个 Entry

 

2. ThreadLocalMap 中 Entry 为什么要继承 WeakReference ?

在 Java 里面存在着强引用、弱引用、软引用和虚引用

A a = new A();
B b = new B();

// 考虑这样的情况
C c = new C(b);
b = null;

对于下面的代码段,我们需要考虑垃圾回收(GC)的情况。当我们将变量 b 设为 null 后,是否意味着经过一段时间后,GC 将会回收变量 b 所分配的内存空间呢?

是否回收的答案是否定的。尽管变量 b 被置为 null,但是变量 c 仍然保持对变量 b 的引用,而且这是一个强引用。因此,GC 不会回收变量 b 原先所分配的内存空间。这将导致内存既无法被释放和利用,也无法被进一步使用,从而产生了内存泄漏的问题。

那么,在这种情况下,我们应该如何防止内存泄漏呢?

一种方法是将变量 c 也设为 null,或者我们可以使用弱引用来引用变量 b,例如使用 WeakReference w = new WeakReference(b)。

基于 Thread、ThreadLocal 和 ThreadLocalMap 之间的关系,我们可以分析它们在堆栈内存中的引用关系,如下图所示:

这也解释了为什么 Entry 继承自 WeakReference,这是一种防止内存泄漏的策略。

ThreadLocal 使用了弱引用,这是否意味着不会出现内存泄漏呢?

如果我们将 ThreadLocal 实例设置为 null,那么意味着堆内存中的 ThreadLocal 实例将不再有强引用指向它,只剩下弱引用。因此,GC 是可以回收这部分内存空间的,这也意味着与该 ThreadLocal 关联的键(key)可以被回收。然而,与值(value)相关联的引用仍然存在,它们是从 Thread 对象引用过来的强引用。因此,只有当 Thread 对象本身被销毁时,与之关联的值(value)才能被释放。

这意味着只要 Thread 对象被 GC 回收,就不会发生内存泄漏。然而,在 ThreadLocal 设置为 null 和线程结束之间的这段时间内,内存仍然不会被回收,这就是我们所说的内存泄漏。尤其是在使用线程池时,由于线程不会被销毁而是被重用,内存泄漏的问题可能会变得更加明显。

那么,ThreadLocal 是如何避免这种情况的呢?

在 ThreadLocalMap 的 set 和 getEntry 方法中,会检查键(key)是否为 null(即 ThreadLocal 是否为 null)。如果是 null 的话,相关联的值(value)会被设为 null。此外,我们还可以通过调用 ThreadLocal 的 remove 方法来显式释放相关资源。

需要注意的是,正常情况下,当不使用线程池时,ThreadLocal 不太可能引发内存泄漏问题。但是,在使用线程池时,情况就取决于线程池的实现。如果线程池没有适当地销毁线程,就可能导致内存泄漏问题。因此,当我们在使用线程池时,必须格外小心处理 ThreadLocal 的使用。

3、ThreadLocal 是如何做到为每一个线程维护变量副本的呢?

经过分析上述内容,我们了解到在 ThreadLocal 类中存在 ThreadLocalMap,用于存储每个线程的局部变量。ThreadLocalMap使用ThreadLocal作为键,存储线程局部变量的值。

ThreadLocal 的set方法

因为每个 Thread 实例都有一个 ThreadLocalMap,所以在执行 set 操作时,首先通过 Thread.currentThread() 获取当前线程,然后利用当前线程 t 调用 getMap(t) 获取 ThreadLocalMap 对象。如果是第一次设置值,ThreadLocalMap 对象为空,因此会进行初始化操作,即调用 createMap(t, value) 方法。

总结一下,set(T value) 方法为每个 Thread 对象创建了一个 ThreadLocalMap,并将 value 放入其中。

ThreadLocal 的get方法


首先获取 ThreadLocalMap 对象,由于 ThreadLocalMap 使用当前的 ThreadLocal 作为键,因此传入的参数为 this。然后调用 getEntry 方法,通过这个键构造索引,根据索引在 table(Entry 数组)中查找线程本地变量。找到对应的 Entry 对象后,判断该 Entry 对象 e 不为空且其引用与传入的键相同,则直接返回值。如果未找到,则调用 getEntryAfterMiss 方法。该方法表示无法直接定位到位置,因此沿着哈希表循环向下查找,从索引 i 开始,一直循环直到找到空槽为止。

4、ThreadLocal 如何进行内存回收?

在 ThreadLocal 层面进行内存回收

当线程终止时,所有保存在线程局部变量中的值会被回收。这里实际上指的是线程对象中的 ThreadLocal.ThreadLocalMap threadLocals 会被回收。

在 ThreadLocalMap 层面进行内存回收

如果线程的生命周期很长,就需要考虑如何在线程生命周期内回收 ThreadLocalMap 的内存。否则,ThreadLocalMap 中保存的 Entry 对象越多,ThreadLocalMap 就会变得越大,占用的内存也会增加。因此,对于不再需要的线程局部变量,应该清理对应的 Entry 对象。

为了解决这个问题,Entry 对象的键使用了 WeakReference 进行包装。当 ThreadLocalMap 的私有数组 table 已经占用了三分之二的位置(即 threshold = 2/3,也就是线程的局部变量超过了10个)时,会尝试回收 Entry 对象。我们可以在 ThreadLocalMap.set() 方法中看到以下代码:

八、总结

ThreadLocal 是一个有用的工具,可以在多线程应用中轻松管理线程局部变量。通过为每个线程提供独立的数据副本,可以避免数据冲突和竞争条件,从而提高应用程序的性能和可靠性。然而,在使用 ThreadLocal 时要小心内存泄漏的问题,务必在不需要时清除数据。

希望本文能够帮助你理解 ThreadLocal 的基本原理和用法,并在实际开发中正确地使用它来处理线程局部变量。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/java/8317.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】