ThreadLocal详解
# ThreadLocal详解
# 1、由同步引出ThreadLocal
我们知道多线程并发访问同一个共享变量时容易出现线程安全问题,特别是在多个线程需要对一个共享变量进行修改操作时。
为了保证线程安全,我们在访问共享变量时需要进行适当的同步操作,比如用synchronized或者Lock。
能不能像JMM中对线程工作内存规定的那样每个线程都有一份变量副本,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量副本呢? ThreadLocal 就可以实现这个功能。 相当于每个线程都有一个自己的小抽屉,
这个抽屉内可以保存自己线程的ThreadLocal 变量,和其他线程无关。
创建一个ThreadLocal 变量,访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个 ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存。
# 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>{}
不过ThreadLocal 有几个内部类比如:SuppliedThreadLocal
、ThreadLocalMap
、Entry
下面详细分析ThreadLocal 内部结构。
# ThreadLocal
的set
方法
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数据结构
也可以利用反射断点看下t1线程的内部情况:
t1线程的 ThreadLocalMap 内部的 table结构。
# ThreadLocal
的get
方法
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的垃圾回收相关。
- 强引用 (Strong Reference)
这是 Java 的默认引用类型。任何通过普通变量引用的对象都是强引用。例如:
Object obj = new Object();
特点:
只要某个对象存在强引用,它就不会被垃圾回收器回收。
当内存不足时,Java 虚拟机宁愿抛出 OutOfMemoryError,也不会回收这些强引用对象。
- 软引用 (Soft Reference)
软引用用于描述一些有用但并非必需的对象。在内存足够时,软引用的对象不会被回收;但在内存不足时,它们会被回收以释放内存。
用法:
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get(); // 获取被软引用的对象
特点:
软引用适合用于缓存,例如浏览器中的页面缓存。
在系统将要发生内存溢出之前,会将这些对象列入回收范围,并且会在回收之前将它们添加到引用队列中。
- 弱引用 (Weak Reference)
弱引用是用来描述非必需对象,强引用消失后,弱引用对象就会被垃圾回收器回收。与软引用相比,只要垃圾回收器运行,不论内存是否充足,都会回收弱引用的对象。 用法:
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get(); // 获取被弱引用的对象
特点:
弱引用适合用于某些不常用的对象,例如映射表中的键。
这种引用类型的对象更容易被垃圾回收。
- 虚引用 (Phantom Reference)
虚引用仅用于跟踪对象的销毁。它的唯一作用是在对象被垃圾回收器回收时收到一个系统通知。
用法:
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());
Object obj = phantomRef.get(); // 永远返回null
特点:
虚引用对象永远无法通过 get() 方法访问。
它必须与 ReferenceQueue 一起使用,当对象被回收时,虚引用会被放入引用队列。
# ThreadLocalMap
的内部类Entry
在 ThreadLocalMap 中,Entry 继承自 WeakReference。因此,Entry 具有一些特定的属性,如下所示:
主要属性如下:
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
四眼这个变量的key被回收了是因为,发生了gc,并且new ThreadLocal<>().set("四眼");
只是创建了ThreadLocal对象,并没有任何引入指向这个对象。
秀逗这个变量的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为null
的Entry
(也可以叫过期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
InheritableThreadLocal
是 ThreadLocal
的一个子类,允许子线程继承父线程中的 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