ThreadLocal详解

# ThreadLocal详解

# 1、由同步引出ThreadLocal

我们知道多线程并发访问同一个共享变量时容易出现线程安全问题,特别是在多个线程需要对一个共享变量进行修改操作时。
为了保证线程安全,我们在访问共享变量时需要进行适当的同步操作,比如用synchronized或者Lock。

能不能像JMM中对线程工作内存规定的那样每个线程都有一份变量副本,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量副本呢? ThreadLocal 就可以实现这个功能。 相当于每个线程都有一个自己的小抽屉,
这个抽屉内可以保存自己线程的ThreadLocal 变量,和其他线程无关。

创建一个ThreadLocal 变量,访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个 ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存。

mixureSecure

# 2、ThreadLocal 简单使用示例

public class TestA {

    static ThreadLocal<String> localVariable = new ThreadLocal<>();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            localVariable.set("秀逗");
            print();
        }, "t1");
        t1.start();


        Thread t2 = new Thread(() -> {
            localVariable.set("四眼");
            print();
        }, "t2");
        t2.start();
    }

    static void print(){
        System.out.println(Thread.currentThread().getName()+localVariable.get());
    }


}

运行结果:

t1秀逗
t2四眼

# 3、ThreadLocal 的实现原理

# ThreadLocal 的继承结构和类属性

类继承结构比较简单

public class ThreadLocal<T>{}
mixureSecure

不过ThreadLocal 有几个内部类比如:SuppliedThreadLocalThreadLocalMapEntry

mixureSecure

下面详细分析ThreadLocal 内部结构。

# ThreadLocalset方法

ThreadLocal的set方法

/**
 * 设置当前线程中与此ThreadLocal关联的值。
 * 如果当前线程已经有一个ThreadLocalMap,那么更新其中的值。
 * 否则,创建一个新的ThreadLocalMap并存储值。
 *
 * @param value 要存储的值
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap  
    ThreadLocalMap map = getMap(t);
    // 如果ThreadLocalMap不为空,则设置值
    if (map != null)
        map.set(this, value);
    // 如果ThreadLocalMap为空,则创建新的ThreadLocalMap并设置值
    else
        createMap(t, value);
}

ThreadLocal.threadLocals变量和getMap方法

// 当前线程的ThreadLocalMap,初始值为null
// Thread类中的一个变量
ThreadLocal.ThreadLocalMap threadLocals = null;

/**
 * 获取指定线程的ThreadLocalMap。
 *
 * @param t 目标线程
 * @return 目标线程的ThreadLocalMap
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

ThreadLocalMap的set方法

/**
 * 在ThreadLocalMap中设置键值对。如果键已存在则替换旧值,
 * 否则插入新键值对。如果发现陈旧的条目,则替换它们。
 *
 * @param key ThreadLocal的键
 * @param value 关联的值
 */
private void set(ThreadLocal<?> key, Object value) {
    // 获取表格数组
    Entry[] tab = table;
    int len = tab.length;
    // 根据key的哈希值确定索引位置
    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();
}

createMap方法

/**
 * 为指定线程创建一个新的ThreadLocalMap并存储初始值。
 *
 * @param t 目标线程
 * @param firstValue 要存储的初始值
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap的构造方法

/**
 * 创建一个新的ThreadLocalMap并将第一个键值对存储在其中。
 *
 * @param firstKey 初始键
 * @param firstValue 初始值
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    // 根据firstKey的哈希值确定索引位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 将初始键值对存储在表格数组中
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

总结:
set方法首先检查当前线程是否已经有一个ThreadLocalMap实例,如果有则更新其中的值,如果没有则创建一个新的ThreadLocalMap并存储初始值。
ThreadLocalMap的set方法则负责将键值对插入到合适的位置,并处理可能存在的键冲突和更新旧数据。

通过上面方法的分析就可以知道ThreadLocal的数据结构如下:
每个Thread类都有一个类型为ThreadLocal.ThreadLocalMap的变量threadLocals。
对应代码ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal的内部类ThreadLocalMap是个简单的键值对存储集合和HashMap有所区别(ThreadLocalMap是数组结构,
每个数组元素是一个Entry extends WeakReference<ThreadLocal<?>> ,并不像HashMap那样是数组+链表\红黑树结构)。

对于下面代码画个图示来表示ThreadLocal的数据结构:

public class TestA {

    static ThreadLocal<String> a = new ThreadLocal<>();
    static ThreadLocal<String> b = new ThreadLocal<>();
    static ThreadLocal<String> c = new ThreadLocal<>();
    static ThreadLocal<String> d = new ThreadLocal<>();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            a.set("秀逗");
            b.set("四眼");
            c.set("大黄");
            d.set("小黑");
        }, "t1");
        t1.start();

    }
}

# 图解ThreadLocal数据结构

mixureSecure

也可以利用反射断点看下t1线程的内部情况:
t1线程的 ThreadLocalMap 内部的 table结构。

mixureSecure

# ThreadLocalget方法

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 从map中获取当前ThreadLocal对象对应的条目
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 返回条目中的值
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map为空或者没有对应的条目,则设置初始值并返回
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    // 返回当前线程的ThreadLocalMap
    return t.threadLocals;
}

ThreadLocalMap 的getEntry方法

private Entry getEntry(ThreadLocal<?> key) {
    // 计算key的hash值在table中的索引
    int i = key.threadLocalHashCode & (table.length - 1);
    // 获取对应索引的条目
    Entry e = table[i];
    // 如果条目不为空并且条目的key等于当前key,则返回该条目
    if (e != null && e.get() == key)
        return e;
    else
        // 否则调用getEntryAfterMiss方法继续查找
        return getEntryAfterMiss(key, i, e);
}

ThreadLocalMap 的getEntryAfterMiss方法

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            // 找到匹配的条目
            return e;
        if (k == null)
            // 清除过期条目
            expungeStaleEntry(i);
        else
            // 计算下一个索引
            i = nextIndex(i, len);
        e = tab[i];
    }
    // 如果没有找到,返回null
    return null;
}

ThreadLocal的setInitialValue方法

private T setInitialValue() {
    // 获取初始值
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 如果map不为空,则设置当前ThreadLocal对象的值
        map.set(this, value);
    else
        // 如果map为空,创建一个新的ThreadLocalMap并设置值
        createMap(t, value);
    // 返回初始值
    return value;
}

总结

  • get() 方法首先获取当前线程,然后尝试从当前线程的 ThreadLocalMap 中获取与当前 ThreadLocal 实例关联的值。
    如果找到了该值,则直接返回;否则调用 setInitialValue() 设置并返回初始值。

  • getMap(Thread t) 方法返回当前线程的 ThreadLocalMap。

  • getEntry(ThreadLocal<?> key) 方法通过计算哈希值获取与 key 关联的条目,如果没有找到,则调用 getEntryAfterMiss 继续查找。

  • getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) 方法在哈希冲突时,通过线性探测法继续查找匹配的条目。

  • setInitialValue() 方法设置当前 ThreadLocal 实例的初始值,如果当前线程的 ThreadLocalMap 为空,则创建一个新的 ThreadLocalMap 并设置初始值。

# ThreadLocalMap的一些性质

ThreadLocalMap是ThreadLocal 的内部类,是真正存储数据的类。其内部还有一个Entry内部类。

ThreadLocalMap内部数据结构 :ThreadLocalMap 使用一个 Entry 数组来存储数据。

默认容量 :ThreadLocalMap 的默认初始容量为 16。

扩容阈值:当 ThreadLocalMap 中的元素数量达到容量的 2/3 时,会触发扩容操作。

每次扩容多少:每次扩容时,ThreadLocalMap 的容量会翻倍。这与大多数哈希表的扩容策略类似,以减少哈希冲突并保持良好的性能。

Hash算法:使用自定义的哈希算法,基于 ThreadLocal 的 threadLocalHashCode 和斐波那契散列增量,确保哈希分布均匀。

对于ThreadLocalMap集合的详细分析可以参考 https://juejin.cn/post/6844904151567040519 (opens new window)
比如解决hash冲突之类的问题。

# ThreadLocalMap原理总结

每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象 ①、调用 set 方法,是以ThreadLocal变量自身作为 key,资源对象作为 value,放入当前线程的ThreadLocalMap 集合中。

 static ThreadLocal<String> localVariable = new ThreadLocal<>();
 // localVariable作为key, 秀逗作为value
 localVariable.set("秀逗");

②、调用 get方法,是以ThreadLocal变量自身作为 key,到当前线程的ThreadLocalMap集合中查找关联的资源值。

③、调用remove 方法,是以ThreadLocal变量自身作为 key,移除当前线程的ThreadLocalMap集合中关联的值。

# 4、ThreadLocal 内存泄漏问题

我们看下ThreadLocalMap的源码:

   static class ThreadLocalMap {
		
		// ThreadLocalMap 的静态内部类 继承WeakReference 弱引用  
		// 用于存储 线程的  本地变量 ThreadLocal 
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        
        // Entry[]数组的初始容量是16
        private static final int INITIAL_CAPACITY = 16;

		// Entry[]数组 用于存储 Entry对象 
        private Entry[] table;
        
        // ThreadLocalMap 的元素个数
     	private int size = 0;

        // 扩容阈值
        private int threshold; // Default to 0
}

# 补充知识点:

# Java中的四种引用类型: 强、软、弱、虚引用。

Java 中提供了四种引用类型,用于不同的内存管理策略:强引用、软引用、弱引用和虚引用。它们的区别在于垃圾回收器对这些引用所关联对象的处理方式。
也就是说强、软、弱、虚引用主要和JVM的垃圾回收相关。

  1. 强引用 (Strong Reference)
    这是 Java 的默认引用类型。任何通过普通变量引用的对象都是强引用。例如:
Object obj = new Object();

特点:
只要某个对象存在强引用,它就不会被垃圾回收器回收。
当内存不足时,Java 虚拟机宁愿抛出 OutOfMemoryError,也不会回收这些强引用对象。

  1. 软引用 (Soft Reference)
    软引用用于描述一些有用但并非必需的对象。在内存足够时,软引用的对象不会被回收;但在内存不足时,它们会被回收以释放内存。
    用法:
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get(); // 获取被软引用的对象

特点:
软引用适合用于缓存,例如浏览器中的页面缓存。
在系统将要发生内存溢出之前,会将这些对象列入回收范围,并且会在回收之前将它们添加到引用队列中。

  1. 弱引用 (Weak Reference)
    弱引用是用来描述非必需对象,强引用消失后,弱引用对象就会被垃圾回收器回收。与软引用相比,只要垃圾回收器运行,不论内存是否充足,都会回收弱引用的对象。 用法:
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get(); // 获取被弱引用的对象

特点:
弱引用适合用于某些不常用的对象,例如映射表中的键。
这种引用类型的对象更容易被垃圾回收。

  1. 虚引用 (Phantom Reference)
    虚引用仅用于跟踪对象的销毁。它的唯一作用是在对象被垃圾回收器回收时收到一个系统通知。
    用法:
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());
Object obj = phantomRef.get(); // 永远返回null

特点:
虚引用对象永远无法通过 get() 方法访问。
它必须与 ReferenceQueue 一起使用,当对象被回收时,虚引用会被放入引用队列。

# ThreadLocalMap的内部类Entry

在 ThreadLocalMap 中,Entry 继承自 WeakReference。因此,Entry 具有一些特定的属性,如下所示:

mixureSecure

主要属性如下:

value
类型:Object
说明:存储 ThreadLocal 对象的值。在上图中,value 被设置为 "四眼"。  
该属性可以理解为ThreadLocalMap中存储的`Entry`对象的value。  

referent
类型:ThreadLocal
说明:作为 WeakReference 的 referent,也即弱引用指向的对象。在上图中,referent 是 ThreadLocal 对象(ThreadLocal@678)。
继承自 WeakReference 的属性,WeakReference 又继承自Reference抽象类的属性。   
该属性可以理解为ThreadLocalMap中存储的`Entry`对象的key。  

queue  
类型:ReferenceQueue  
说明:引用队列。当弱引用指向的对象被垃圾回收器回收时,弱引用自身会被放入这个引用队列中。

next
类型:Reference
说明:用于引用队列中的链表结构,指向下一个引用。

discovered
类型:Reference
说明:在某些 GC 实现中用于处理引用队列。  

# 可能造成内存泄漏的原因:

当 referent 被回收后,Entry 的键变为 null,但是 value 仍然占用内存。这些未被清理的值会导致内存泄漏,尤其是在线程池中使用 ThreadLocal 时,线程对象长时间不被销毁,泄漏的可能性更大。

举个例子:

static ThreadLocal<String> a = new ThreadLocal<>();
  public static void main(String[] args) throws  Exception {
        Thread t1 = new Thread(() -> {
            a.set("秀逗");
            new ThreadLocal<>().set("四眼");
            LockSupport.park();
        }, "t1");
        t1.start();
        Thread.sleep(2000);
        System.gc();
    }

我们反射断点看下t1线程内部的table

mixureSecure

四眼这个变量的key被回收了是因为,发生了gc,并且new ThreadLocal<>().set("四眼");只是创建了ThreadLocal对象,并没有任何引入指向这个对象。

mixureSecure

秀逗这个变量的key没有回收是因为static ThreadLocal<String> a = new ThreadLocal<>();这里的a是个强引用,即使发生了gc,也不会导致referent被回收。

如果线程 t1 一直运行,并且 ThreadLocal 对象的键(referent)已经被回收,那么它的值(value)不会自动被回收。这是因为 ThreadLocalMap 中的 Entry 对象还持有对值(四眼)的强引用,导致内存泄漏。

# ThreadLocal 的remove方法

ThreadLocal 的remove方法

public void remove() {
    // 获取当前线程的 ThreadLocalMap 实例
    ThreadLocalMap m = getMap(Thread.currentThread());
    
    // 如果当前线程的 ThreadLocalMap 不为空,则从中移除当前 ThreadLocal 实例
    if (m != null) {
        m.remove(this);
    }
}

ThreadLocalMap 的remove方法

private void remove(ThreadLocal<?> key) {
    // 获取内部的 Entry 数组
    Entry[] tab = table;
    int len = tab.length;
    // 计算 key 的哈希码在数组中的索引
    int i = key.threadLocalHashCode & (len - 1);

    // 遍历数组,找到与 key 关联的 Entry
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 如果找到目标 key
        if (e.get() == key) {
            // 清除该 Entry 的 value
            e.clear();
            // 清除过期的 Entry
            expungeStaleEntry(i);
            return;
        }
    }
}

# 清除过期的 Entry

在 ThreadLocalMap 中,过期的 Entry 出现主要是因为 ThreadLocal 实例被垃圾回收(GC)了,但是对应的 Entry 还留在 ThreadLocalMap 中。这种情况会导致内存泄漏,因为这些 Entry 仍然引用着可能不再需要的对象。

过期 Entry 出现的原因:
ThreadLocal 实例被回收: 当一个 ThreadLocal 实例不再被任何地方引用时,GC 会回收这个 ThreadLocal 实例。由于 ThreadLocalMap 中的 Entry 使用 WeakReference 持有 ThreadLocal 的键,当 ThreadLocal 被回收后,WeakReference 会返回 null。

ThreadLocalMap 中的 Entry 没有及时清理: ThreadLocalMap 的 Entry 数组中可能仍然存在对被回收 ThreadLocal 键的引用,但这些键已经变成了 null,所以这些 Entry 变成了“过期”的 Entry。

清除过期的 Entry 的方法 expungeStaleEntry

private int expungeStaleEntry(int staleSlot) {
    // 获取内部的 Entry 数组
    Entry[] tab = table;
    int len = tab.length;

    // 清除 staleSlot 位置的 entry
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;

    // 从 staleSlot 的下一个位置开始,重新哈希直到遇到 null
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        if (k == null) {
            // 清除无效的 entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 重新计算哈希位置
            int h = k.threadLocalHashCode & (len - 1);

            // 如果 entry 的哈希位置不正确,则移动到正确的位置
            if (h != i) {
                tab[i] = null;

                // 扫描直到遇到 null
                while (tab[h] != null)
                    h = nextIndex(h, len);

                tab[h] = e;
            }
        }
    }

    return i;
}

总结:
expungeStaleEntry 方法通过清除过期的 Entry 并重新哈希剩余的 Entry,确保 ThreadLocalMap 的有效性和性能。这种机制防止了内存泄漏,同时保证了 ThreadLocal 变量的正确管理。

# 建议ThreadLocal 使用完毕后手动remove

在使用 ThreadLocal 变量时,建议在使用完毕后显式调用 remove() 方法。这有助于避免内存泄漏,确保系统资源能够被及时释放。

# 为什么ThreadLocalMap中的key要设计为弱引用?

首先明确ThreadLocalMap中的key就是ThreadLocal类的实例,ThreadLocal类本身并不是弱引用,只是ThreadLocal类的实例作为key封装成Entry对象的时候会被封装成弱引用。
对应源码:

 static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
			// Entry构造方法 会把ThreadLocal<?> k 封装成弱引用。
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
public WeakReference(T referent) {
        super(referent);
    }

由于线程可能需要长时间运行(比如线程池里的线程)。如果 key不再使用,需要在内存不足的时候释放其占用的内存(通过垃圾回收机制)。
问题在于ThreadLocalMap的key是弱引用,value是强引用, GC 仅是让 key 的内存释放,后续还要根据 key 是否为 null 来进一步释放值的内存。 就像上面remove方法中的expungeStaleEntry方法用于清除key为nullEntry(也可以叫过期Entry)。 除了调用remove方法能主动释放外,还有两个释放时机。
比如:
获取 key 时发现 null key。
set key 时,会使用启发式扫描,清除临近的 null key,启发次数与元素个数,是否发现 null key 有关。

具体清除过期key的详细分析可以参考 https://juejin.cn/post/6844904151567040519 (opens new window)

# 5、ThreadLocal 的适用场景

下面列举几个具体的使用场景:

# ①、存储用户会话数据:

在 Web 应用中,ThreadLocal 可以用于存储用户会话数据,比如用户的身份信息、权限等。每个线程处理不同的用户请求时,可以通过 ThreadLocal 来隔离不同用户的数据。这是个非常典型的ThreadLocal 应用场景,我公司的通用网关服务中就是使用ThreadLocal 来存储会话相关的线程变量。使用起来非常方便。

具体的做法:
先利用ThreadLocal封装一个线程上下文处理器。
下面这个ContextHandler 为了适应业务封装了比较多信息。

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 线程上下文处理器  
 * 主要用于保存用户线程的会话信息
 */
public class ContextHandler {

    // 泛型使用Map<String, Object> 方便灵活保存各种信息
    public static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();

    /**
     * 用户ID
     */
    private static final String CONTEXT_KEY_USER_ID = "userId";

    /**
     * 登录名 
     */
    private static final String CONTEXT_KEY_USERNAME = "username";

    /**
     * 姓名 
     */
    private static final String CONTEXT_KEY_NAME = "name";

    /**
     * token
     */
    private static final String CONTEXT_KEY_TOKEN = "token";

    /**
     * 权限集合
     */
    private static final String CONTEXT_KEY_PERMISSION = "permission";

    /**
     * 超级管理员(系统默认最高管理员)
     */
    private static final String CONTEXT_KEY_SUPER_ADMIN = "superAdmin";

    /**
     * 角色编码
     */
    private static final String CONTEXT_KEY_ROLE_CODE = "roleCode";

    /**
     * 数据源连接(动态数据源切换时使用)
     */
    private static final String CONTEXT_KEY_DS_CONN = "dsConn";


    public static void set(String key, Object value) {
        Map<String, Object> map = threadLocal.get();
        if (map == null) {
            map = new HashMap<String, Object>(6);
            threadLocal.set(map);
        }
        map.put(key, value);
    }

    public static Object get(String key) {
        Map<String, Object> map = threadLocal.get();
        if (map == null) {
            map = new HashMap<String, Object>(6);
            threadLocal.set(map);
        }
        return map.get(key);
    }

    public static String getUserId() {
        Object value = get(CONTEXT_KEY_USER_ID);
        return returnObjectValue(value);
    }

    public static String getUsername() {
        Object value = get(CONTEXT_KEY_USERNAME);
        return returnObjectValue(value);
    }

    public static String getName() {
        Object value = get(CONTEXT_KEY_NAME);
        return returnObjectValue(value);
    }

    public static String getToken() {
        Object value = get(CONTEXT_KEY_TOKEN);
        return getObjectValue(value);
    }

    public static String getPermission() {
        Object value = get(CONTEXT_KEY_PERMISSION);
        return getObjectValue(value);
    }

    public static boolean getSuperAdmin() {
        Object value = get(CONTEXT_KEY_SUPER_ADMIN);
        return Boolean.parseBoolean(getObjectValue(value));
    }

    public static String getRoleCode() {
        Object value = get(CONTEXT_KEY_ROLE_CODE);
        return getObjectValue(value);
    }

    public static String getDsConn() {
        Object value = get(CONTEXT_KEY_DS_CONN);
        return getObjectValue(value);
    }

    public static void setUserId(String userId) {
        set(CONTEXT_KEY_USER_ID, userId);
    }

    public static void setUsername(String username) {
        set(CONTEXT_KEY_USERNAME, username);
    }

    public static void setName(String name) {
        set(CONTEXT_KEY_NAME, name);
    }

    public static void setToken(String token) {
        set(CONTEXT_KEY_TOKEN, token);
    }

    public static void setPermission(Map<String, List<String>> userPermission) {
        set(CONTEXT_KEY_PERMISSION, userPermission);
    }

    public static void setSuperAdmin(boolean isSupperAdmin) {
        set(CONTEXT_KEY_SUPER_ADMIN, isSupperAdmin);
    }

    public static void setRoleCode(String roleCode) {
        set(CONTEXT_KEY_ROLE_CODE, roleCode);
    }

    public static void setDsConn(String dsConn) {
        set(CONTEXT_KEY_DS_CONN, dsConn);
    }

    public static void remove() {
        threadLocal.remove();
    }


    private static String returnObjectValue(Object value) {
        return value == null ? null : value.toString();
    }

    private static String getObjectValue(Object obj) {
        return obj == null ? "" : obj.toString();
    }

}

在网关服务的过滤器中对每个用户请求线程设置ContextHandler所需要的值。
我们使用的基础网关是com.netflix.zuul

@Component
public class AuthZuulFilter extends ZuulFilter {

	 @Override
    public int filterOrder() {
        return 1;
    }

  @Override
    public boolean shouldFilter() {
       // ...  过滤规则省略
	}

	@Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.getResponse().setCharacterEncoding("UTF-8");
        HttpServletRequest request = ctx.getRequest();

        logger.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());

        String token = request.getHeader(JwtConstant.JWT_AUTH_HEADER);
        // 这里主要是为了方便调试 加了个token的后门
		if (StringUtils.isBlank(token)) {
            token = request.getParameter("_token");
        }

        /*
         * 校验权限——登录
         */
        String userId;
        try {
            //检查jwt令牌, 如果令牌不合法或者过期, 里面会直接抛出异常, 下面的catch部分会直接返回
            Account auth = tokenFactory.validateToken(token);
            userId = auth.getUserId();
			// 如果登录成功 就添加当前登录线程的会话信息到 ContextHandler ,最终是存储到线程的ThreadLocal变量中
            ContextHandler.setUserId(auth.getUserId());
            ContextHandler.setUsername(auth.getUsername());
            ContextHandler.setSuperAdmin(auth.getLoginName().equals(superAdmin));
            
        } catch (UnAuthorizedException e) {
            // ... 省略
            return null;
        } catch (Exception e) {
           // ... 省略
            return null;
        }
		// ... 省略
        return null;
    }
}

每次会话结束后再主动remove,防止内存泄露
利用Spring 提供的OncePerRequestFilter过滤器确保每个 HTTP 请求只被过滤器处理一次。

@Component
public class ContextFilterProcessor extends OncePerRequestFilter {
	
	 @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws IOException {
        try {
			// ... 省略
            log.info("current request url: {}", request.getServletPath());
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            log.error("请求异常", e);
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED, "请求异常");
        } finally {
			// 在 finally 块中 remove ThreadLocal变量 防止内存泄露
            ContextHandler.remove();
        }
    }
}

ContextHandler使用起来非常简单方便: 对于一些需要做人员数据权限的场景来说很好用。

// 获取当前用户的id
String userId = ContextHandler.getUserId();

只不过ThreadLocal 在异步场景下子线程无法共享父线程中创建的ThreadLocal变量数据。如果子线程也需要父线程中的ThreadLocal变量,需要手动在子线程中设置。

这个在下面第6节会详细说到。

# ②、事务管理

在数据库操作中,ThreadLocal 可以用于管理事务。例如,在每个线程中维护事务的状态或数据库连接,使得在同一线程中的操作共享相同的事务上下文。
下面是简单的示例伪代码:

public class TransactionManager {
    private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();

    public static Connection getConnection() {
        return connectionHolder.get();
    }

    public static void setConnection(Connection connection) {
        connectionHolder.set(connection);
    }

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

# ③、日志上下文

ThreadLocal 可以用于在日志记录中存储线程特定的信息,如日志上下文或跟踪 ID。这有助于在分布式系统中追踪和调试问题。
如果做链路追踪可以考虑。
下面是简单的示例伪代码:

public class LogContext {
    private static final ThreadLocal<String> requestId = new ThreadLocal<>();

    public static void setRequestId(String id) {
        requestId.set(id);
    }

    public static String getRequestId() {
        return requestId.get();
    }

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

# ④、使用副本变量保证线程安全的场景

使用 ThreadLocal 来管理线程局部变量是解决线程安全问题的一种有效方法。特别是在需要确保每个线程都有自己独立的实例时,例如在处理线程不安全的对象(如 SimpleDateFormat)时,利用ThreadLocal 来存储 DateFormat(SimpleDateFormat的父类) 实例,以确保每个线程都拥有自己独立的 SimpleDateFormat实例,从而避免了线程间的竞态条件和不一致性。

import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class DateUtils {
    public static final ThreadLocal<DateFormat> simpleDateFormat = new ThreadLocal<DateFormat>(){
        // 利用重新 initialValue() 方法来创建每个线程的DateFormat初始值
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}

使用也非常简单:

ThreadLocal<DateFormat> simpleDateFormat = DateUtils.simpleDateFormat;
DateFormat dateFormat = simpleDateFormat.get();
String format = dateFormat.format(new Date());

# 6、注意:ThreadLocal 不支持继承性

# 问题演示

这里说的继承性是指在父线程内创建子线程,那么子线程无法获取父线程中的ThreadLocal变量。
这意味着在创建子线程时,子线程不会自动继承父线程中的 ThreadLocal 变量。这是 ThreadLocal 的设计中的一个重要考虑点,通常是为了避免线程间的隐式依赖和共享状态。

这里我们使用上面第5节第①小点的会话变量来演示下:

public class TestA {

    public static void main(String[] args) throws Exception {
        String userId = "123";
        // 主线程中设置 用户id
        ContextHandler.setUserId(userId);

        // 在主线程中创建子线程 t1
        Thread t1 = new Thread(() -> {
            System.out.println("子线程中获取用户id: " + ContextHandler.getUserId());
        }, "t1");
        t1.start();

        // 主线程中获取用户id
        System.out.println("主线程中获取用户id: " + ContextHandler.getUserId());
        
    }

}

运行结果:

主线程中获取用户id: 123
子线程中获取用户id: null

ThreadLocal 不支持继承性 这种设计只能说是一种特性,因为的确有时候并不需要线程之间进行ThreadLocal 副本数据的传递。
但是有些时候又是需要传递的。 比如上面的例子中我需要在t1线程中获取当前登录人id的情况。

# 解决方案

有两种解决方案可以实现子线程能够获取到相应的ThreadLocal 副本数据:
①、直接设置子线程的副本变量
优点:这种方式简单直接,而且灵活。 缺点: 如果忘记设置,可能会造成业务代码出错。
代码示例:

public class TestA {

    public static void main(String[] args) throws Exception {
        String userId = "123";
        // 主线程中设置 用户id
        ContextHandler.setUserId(userId);

        // 在主线程中创建子线程 t1
        Thread t1 = new Thread(() -> {
            // 手动设置子线程的useId副本变量
            ContextHandler.setUserId(userId);
            System.out.println("子线程中获取用户id: " + ContextHandler.getUserId());
        }, "t1");
        t1.start();

        // 主线程中获取用户id
        System.out.println("主线程中获取用户id: " + ContextHandler.getUserId());

    }

}

运行结果:

主线程中获取用户id: 123
子线程中获取用户id: 123

我们生产使用的是第一种方式,需要自己设置子线程的副本变量,虽然麻烦了一些,但是对于处理复杂的数据权限业务来说比较灵活。同时如果把上面封装的 ContextHandler 作为公司的框架级服务,需要在代码规范中强调 封装的ThreadLocal 不支持继承性以免造成代码编写错误。

②、使用InheritableThreadLocal

InheritableThreadLocalThreadLocal 的一个子类,允许子线程继承父线程中的 ThreadLocal 变量。与 ThreadLocal 不同,InheritableThreadLocal 使得在创建子线程时,子线程可以自动获得父线程中的 InheritableThreadLocal 变量的值。 代码示例:

public class TestA {

    public static void main(String[] args) throws Exception {
        InheritableThreadLocal<String> userId = new InheritableThreadLocal<>();
        userId.set("123");
        // 在主线程中创建子线程 t1
        Thread t1 = new Thread(() -> {
            System.out.println("子线程中获取用户id: " + userId.get());
        }, "t1");
        t1.start();

        // 主线程中获取用户id
        System.out.println("主线程中获取用户id: " + userId.get());
    }

}

运行结果:

主线程中获取用户id: 123
子线程中获取用户id: 123

还是建议使用方式①,自己设置值,比较灵活。

参考资料:
《Java并发编程之美》
https://juejin.cn/post/6844904151567040519
https://pdai.tech/md/java/thread/java-thread-x-threadlocal.html