ThreadLocal的使用场景和原理

124 阅读6分钟

1 ThreadLocal是什么

  • 顾名思义,ThreadLocal 是线程本地存储,又叫做本地线程局部变量
  • ThreadLocal 中填充的的是当前线程的变量,为每一个线程都提供了变量的副本,某个线程对该变量进行使用,使用的即是该线程的局部变量(即该变量的副本),使得不同线程在某一时刻访问到的不是同一个变量,解决多线程的并发安全问题,保证了线程的安全性。

2 ThreadLocal 的使用场景

  • 数据源管理

    • ThreadLocal 能够确保每个线程都有自己独立的变量副本,可以实现线程封闭,有效避免了多线程并发访问共享变量可能带来的竞态条件和数据不一致问题。
    • 例如在使用 JDBC 进行数据库操作的时候,为了确保每个线程可以独立地管理自己的数据库连接,可以使用 ThreadLocal 对java.sql.Connection对象进行封装,防止连接未能及时关闭、连接被多个线程共享等问题。
  • web开发时可以用于传递信息

    • 在web开发过程中,有时我们在controller获取到的用户信息需要传递到service,dao及其他工具类,通过 ThreadLocal,在 Controller 中设置上下文信息后,各个层次的代码( Service、DAO、Util 类等)都可以方便地获取当前线程的上下文信息,而不需要显式地传递参数。
  • 线程安全性问题

    • 对于线程不安全的变量,通过将数据变量存储在 ThreadLocal 中,每个线程都可以独立地访问和修改自己的数据变量副本,避免了多线程并发访问共享变量可能导致的线程安全问题。
    • 例如,在工具类中,我们有一个计数器方法,里面有一个计数器变量用于记录当前操作的执行次数,在多线程的情况下,可能有其他线程同时对该变量进行操作,从而影响本线程的计数值。此时我们可以使用 ThreadLocal 封装计数器变量。每个线程通过调用 add() 方法递增的是自己的计数器变量副本,而不会影响其他线程的计数器值。

ThreadLocal 的原理

  • ThreadLocal 的实现原理其实比较简单,核心是在 Thread 类中维护了一个 ThreadLocalMap 对象,用于存储当前线程对应的 ThreadLocal 变量及其值。

  • 在 ThreadLocalMap 对象中,每个 ThreadLocal 实例都作为 key,对应着一个 value,即当前线程对应的变量副本。

  • 在进行 get()、set() 操作时,ThreadLocal 会先获取当前线程,然后通过当前线程获取到其对应的 ThreadLocalMap,从而取出、设置对应的 value 值。

  • 在ThreadLocal的 set() 方法中,先从当前线程的 ThreadLocalMap 中获取 Entry,如果存在则更新其 value 值,否则会创建一个 ThreadLocalMap ,添加 ThreadLocal - Value 到 ThreadLocalMap 中,并且绑定 ThreadLocalMap 到当前线程。实现每个线程都拥有独立的变量副本

public void set(T value) {
    Thread t = Thread.currentThread(); 
    ThreadLocalMap map = getMap(t); 
    if (map != null) { 
    map.set(this, value); 
    } else { 
        createMap(t, value);
       } 
    }
  • ThreadLocalMap 如何将一个 ThreadLocal 对象与一个初始值关联起来?
    • ThreadLocalMap 将一个 ThreadLocal 对象与一个初始值关联起来的方法是通过 set(ThreadLocal<?> key, Object value) 实现的。
    • set() 方法首先根据 ThreadLocal 对象的哈希值计算要存储的索引位置 i,然后通过循环遍历该位置上的链表。在遍历过程中,如果找到了与 ThreadLocal 对象相等的键(即同一对象),则将对应的值更新为传入的初始值,并返回。如果遇到已经被垃圾回收的键,则调用 replaceStaleEntry() 方法替换该键和值,并返回。
    • 如果循环遍历到链表末尾仍未找到与 ThreadLocal 对象相等的键,则在链表末尾添加一个新的 Entry 对象,将 ThreadLocal 对象和初始值关联起来。随后,增加计数器变量 size,并根据阈值判断是否需要进行再哈希操作或清除已被垃圾回收的 Entry 对象。
public void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len - 1);

    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

  • 在 ThreadLocal的 get() 方法中,首先获取当前线程,然后从当前线程的 ThreadLocalMap 中获取与当前 ThreadLocal 对象对应的 Entry(键值对),并返回相应的 value 值。如果获取不到,则通过 setInitialValue() 方法来设置初始值,并返回该值。
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(); 
       }

总而言之,ThreadLocal 的工作流程如下:

  • 线程创建时,会有一个独立的 ThreadLocalMap 对象与之关联。这个对象是用于存储该线程中所有 ThreadLocal 对象的值。
  • 当需要设置某个线程的 ThreadLocal 变量时,首先获取当前线程的 ThreadLocalMap 对象。
  • 在 ThreadLocalMap 中查找当前 ThreadLocal 对象,找到对应的键值对,如果找到键值对,则更新其值;如果没有找到,则创建新的键值对并插入 ThreadLocalMap 中 -当需要获取某个线程的 ThreadLocal 变量时,同样先获取当前线程的 ThreadLocalMap 对象,在 ThreadLocalMap 中根据 ThreadLocal 对象进行查找,如果找到对应的键值对,则返回其值;否则,返回初始值(如果有设置)或者 null。

ThreadLocal中的内存泄漏问题

  • 通过上述分析我们可以看出,当线程中使用了ThreadLocal变量时,会在每个线程中创建一个对应的ThreadLocalMap对象,用于存储该线程中所有的ThreadLocal变量。ThreadLocalMap对象中的 Entry 是弱引用,并不影响 ThreadLocal 的生命周期。每个 Entry 对象引用着一个 ThreadLocal 变量与该线程中所保存的值(也就是我们调用 ThreadLocal.set() 方法存储的值)。
  • 如果在线程操作完成后,没有释放ThreadLocal变量的引用,而该线程又存在较长时间,那么存储在 ThreadLocalMap 中的引用链就会一直存在,从而导致内存泄漏。因为即使线程已经结束(失去强引用),但是ThreadLocalMap中的值依然会对ThreadLocal对象持有引用(ThreadLocalMap中的Entry对象还维持着对ThreadLocal变量的弱引用),而ThreadLocalMap对象无法被自动回收。如果大量的线程使用ThreadLocal,那么这些内存泄漏可能会进一步加剧系统的内存压力。

避免ThreadLocal中的内存泄漏问题

  • 显式地使用remove()方法:在线程结束时手动调用ThreadLocal的remove()方法,确保释放ThreadLocal中的值。可以通过finally代码块来保证调用remove()方法,即使发生异常也能正确释放资源。