写点什么

浅析 ThreadLocal

作者:java易二三
  • 2023-08-31
    湖南
  • 本文字数:4495 字

    阅读完需:约 15 分钟

前言

作为一枚应届毕业菜鸟,在面试中经常被考察的题目:什么是 ThreadLocal?ThreadLocal 的底层原理?以及在实际开发项目过程中,经常用到保存用户信息的类就是 ThreadLocal。

ThreadLocal 是 Java 编程语言中的一个线程本地变量,它为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。

那么 ThreadLocal 到底解决了什么问题,又适用于什么样的场景?以及 ThreadLocal 一些细节!ThreadLocal 的最佳实践!今天,我们就从这些角度出发,浅析一下 ThreadLocal。

1. ThreadLocal 的原理

ThreadLocal 的基本原理是通过在每个线程中创建一个 ThreadLocal 对象来实现的。该对象内部维护了一个 Map 数据结构,用于存储每个线程对应的 ThreadLocal 变量值。当一个线程需要访问 ThreadLocal 变量时,它会首先获取当前线程对应的 ThreadLocal 对象,然后通过该对象来访问和修改 ThreadLocal 变量的值。由于每个线程都有自己的 ThreadLocal 对象,因此它们之间互不影响,从而实现了线程之间的隔离。

我们如果想要真正理解 ThreadLocal,就得从源码看起。

1.1 ThreadLocal 的 set()方法

ThreadLocal 的 set()方法用于设置当前线程对应的 ThreadLocal 变量的值。

具体来说,set()方法接受一个参数,即要设置的值。当调用 set()方法时,它会首先获取当前线程对应的 ThreadLocal 对象,然后将该值存储到 ThreadLocal 对象中。由于每个线程都有自己的 ThreadLocal 对象,因此它们之间互不影响,从而实现了线程之间的隔离。

scss复制代码public void set(T value) {  //1、获取当前线程  Thread t = Thread.currentThread();  //2、获取线程中的属性 threadLocalMap ,如果 threadLocalMap 不为空,  //则直接更新要保存的变量值,否则创建 threadLocalMap,并赋值  ThreadLocalMap map = getMap(t);  if (map != null)      map.set(this, value);  else      // 初始化 thradLocalMap 并赋值      createMap(t, value);}
复制代码

1.2 ThreadLocal 的 get()方法

ThreadLocal 的 get()方法用于获取当前线程对应的 ThreadLocal 变量的值。

scss复制代码public T get() {  //1、获取当前线程  Thread t = Thread.currentThread();  //2、获取当前线程的 ThreadLocalMap  ThreadLocalMap map = getMap(t);  //3、如果 map 数据不为空,  if (map != null) {      //3.1、获取 threalLocalMap 中存储的值      ThreadLocalMap.Entry e = map.getEntry(this);      if (e != null) {          @SuppressWarnings("unchecked")          T result = (T)e.value;          return result;      }  }  //如果是数据为 null,则初始化,初始化的结果,TheralLocalMap 中存放 key 值为 threadLocal,值为 null  return setInitialValue();}private T setInitialValue() {  T value = initialValue();  Thread t = Thread.currentThread();  ThreadLocalMap map = getMap(t);  if (map != null)      map.set(this, value);  else      createMap(t, value);  return value;}
复制代码

1.3 ThreadLocal 的 remove()方法

ThreadLocal 的 remove()方法用于移除当前线程对应的 ThreadLocal 变量的值。

arduino复制代码public void remove() {  Thread t = Thread.currentThread();  ThreadLocalMap map = getMap(t);  if (map != null) {      map.remove(this);  }}
复制代码

当调用 remove 方法时,ThreadLocal 变量将从当前线程的 ThreadLocalMap 中删除,以便垃圾回收器可以回收它所引用的对象。如果没有调用 remove 方法,则可能会导致内存泄漏问题 。

1.4 ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 类中的一个内部类,用于存储每个线程的 ThreadLocal 变量。具体来说,ThreadLocalMap 是一个 HashMap 对象,其中键为 ThreadLocal 对象,值为对应的 ThreadLocal 变量值。

ThreadLocalMap 的作用是为了实现线程之间的隔离。每个线程都有自己的 ThreadLocalMap 对象,用于存储该线程的 ThreadLocal 变量。当需要访问 ThreadLocal 变量时,首先会通过 get()方法获取对应的 ThreadLocal 对象,然后通过该对象来访问和修改 ThreadLocal 变量的值。如果当前线程没有对应的 ThreadLocalMap 对象,则会先初始化该线程的 ThreadLocalMap 对象,并将该线程的 ThreadLocal 变量存储在 initialValue 字段中。

需要注意的是,由于每个线程都有自己的 ThreadLocalMap 对象,因此它们之间互不影响,从而实现了线程之间的隔离。同时,由于 ThreadLocalMap 对象是由 ThreadLocal 类内部维护的,因此不需要手动进行垃圾回收。

成员变量

arduino复制代码  //初始容量,必须是 2 的整次幂   private static final int INITIAL_CAPACITY = 16;   //存放数据的 Table,长度也必须是 2 的整次幂   private ThreadLocal.ThreadLocalMap.Entry[] table;   //数组里面 entrys 的个数,可以用于判断 table 当前使用量是否超过阈值   private int size = 0;   //进行扩容的阈值,表使用量大于它的时候进行扩容。   private int threshold; // Default to 0
复制代码

存储结构-Entry

scala复制代码static class Entry extends WeakReference<ThreadLocal<?>> {  /** The value associated with this ThreadLocal. */  Object value;  Entry(ThreadLocal<?> k, Object v) {    super(k);    value = v;  }}
复制代码

在 ThreadLocalMap 中,用 Entry 保存 key-v 结构。不过 Entry 中的 key 只能是 ThreadLocal 对象,并且 key 是弱引用,只能活到下次 GC 开始,目的是将 ThreadLocal 对象的生命周期和线程生命周期进行解绑。

2. 一个简单的 ThreadLocal

scala复制代码public class MyThread extends Thread implements Runnable {  private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();  public void run() {    threadLocal.set(10); // 在当前线程中设置变量值为 10    try {        Integer value = threadLocal.get(); // 从当前线程中获取变量值为 10        System.out.println("从当前线程中获取的变量值为:" + value);    } catch (Exception e) {        e.printStackTrace();    } finally {        threadLocal.remove(); // 在当前线程中移除变量值为 10 的 ThreadLocal 对象    }  }}
复制代码

在这个示例中,MyThread 类继承了 Thread 类和 Runnable 接口,并实现了 run()方法。在 run()方法中,我们使用了 ThreadLocal 对象来存储一个整数变量,并在当前线程中设置和获取该变量的值。当创建 MyThread 对象时,会自动调用其构造函数,并将该对象作为参数传递给 Thread 类的构造函数。然后,Thread 类会启动该线程,并调用 MyThread 对象的 run()方法来执行线程任务。

除了继承 Thread 类和实现 Runnable 接口之外,还可以使用匿名内部类来创建 Thread 对象。例如:

csharp复制代码new Thread(new Runnable() {  @Override  public void run() {    threadLocal.set(10); // 在当前线程中设置变量值为 10    try {        Integer value = threadLocal.get(); // 从当前线程中获取变量值为 10        System.out.println("从当前线程中获取的变量值为:" + value);    } catch (Exception e) {        e.printStackTrace();    } finally {        threadLocal.remove(); // 在当前线程中移除变量值为 10 的 ThreadLocal 对象    }  }}).start();
复制代码

在这个示例中,我们使用了匿名内部类来创建 Thread 对象,并将其作为参数传递给 Thread 类的构造函数。然后,我们调用 Thread 对象的 start()方法来启动线程。

3. ThreadLocal 的使用场景

  1. 多线程环境下共享数据,避免数据竞争问题。例如,多个线程需要同时访问同一个计数器、队列等数据结构时,可以使用 ThreadLocal 来为每个线程提供独立的变量副本,从而避免了数据竞争问题。

  2. 用于存储线程上下文信息,如用户 ID、请求 ID 等。例如,在 Web 应用程序中,每个请求都会创建一个 ThreadLocal 对象,用于存储该请求的上下文信息,如用户 ID、请求 ID 等。

  3. 实现线程安全的日志记录功能。例如,在 Web 应用程序中,可以使用 ThreadLocal 来为每个线程提供独立的日志记录器,从而实现线程安全的日志记录功能。

  4. 在测试框架中使用 ThreadLocal 来隔离测试用例之间的数据。例如,在 JUnit 测试框架中,可以使用 ThreadLocal 来为每个测试用例提供独立的测试数据,从而避免了测试用例之间的数据干扰问题。

  5. 最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。代码如下:

    csharp 复制代码

    //数据库连接 private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {    public Connection initialValue() {      return DriverManager.getConnection(DB_URL);   }   };     public static Connection getConnection() {    return connectionHolder.get();   }  java 复制代码//Session 管理 private static final ThreadLocal threadSession = new ThreadLocal(); public static Session getSession() throws InfrastructureException { Session s = (Session) threadSession.get(); try { if (s == null) { s = getSessionFactory().openSession(); threadSession.set(s); } } catch (HibernateException ex) { throw new InfrastructureException(ex); } return s; }

4. ThreadLocal 的一些细节

需要注意的是,由于每个线程都有自己的 ThreadLocal 对象,因此在使用 ThreadLocal 变量时,需要确保每个线程都正确地初始化了自己的 ThreadLocal 对象。否则可能会导致多个线程同时访问同一个 ThreadLocal 变量的情况发生,从而导致数据不一致的问题。

ThreadLocal 还有一个需要注意的潜在风险,就是内存泄露。

内存泄露产生的原因如下:



如图所示存在一条引用链:Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value。

经过上面的讲解我们知道 ThreadLocal 作为 Key,但是被设置成了弱引用。弱引用在 JVM 垃圾回收时是优先回收的,就是说无论内存是否足够弱引用对象都会被回收;弱引用的生命周期比较短;当发生一次 GC 的时候就会变成如下:



ThreadLocalMap 中出现了 Key 为 null 的 Entry,此时没有办法访问这些 key 为 null 的 Entry 的 value。如果线程迟迟不结束,就会由于 value 永远无法回收而造成内存泄露。如果当前线程运行结束 Thread,ThreadLocalMap 和 Entry 之间没有了引用链,在垃圾回收的时候就会被回收。但是在开发中我们都是使用线程池的方式,线程池的复用不会主动结束,所以还是会存在内存泄露问题。

这个问题解决方法也很简单,就是在我们在使用完 ThreadLocal 方法后手动调用 remove()方法清除数据。在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。

用户头像

java易二三

关注

还未添加个人签名 2021-11-23 加入

还未添加个人简介

评论

发布
暂无评论
浅析ThreadLocal_Java_java易二三_InfoQ写作社区