今天来讲下 ThreadLocal 这个东西很常见, 在我们做Web 开发的时候,经常将一个请求线程相关的用户信息(如 UserId)存入 ThreadLocal,在后续的 Service、Dao 层都可以直接获取,无需在方法参数中层层传递。
使用场景
比如, 我们在定义Controller 的时候, 通过@RequestHeader 来获取请求头部的指定信息:
@PostMapping("/login")
public Response> userLogin(@RequestHeader("userId") String userId) {
log.info("===> 请求头的userId: {}", userId)
...
}
上面是一个简单的接口的定义。我们从请求头中获取 userId 这个属性, 从而进行后续业务处理, 不过这种方式就得在 所有需要从请求头中获取userId 的Controller中专门声明 @RequestHeader("userId")String userId 这样一段, 是不是太麻烦了? 所以我们可以通过 Filter+ThreadLocal来简化这个操作。
首先定义一下 ThreadLocal
package com.kitten.auth.filter;
import ...
public class LoginUserContextHolder {
// 初始化一个 ThreadLocal 变量
private static final ThreadLocal<Map<String, Object>> LOGIN_USER_CONTEXT_THREAD_LOCAL
= ThreadLocal.withInitial(HashMap::new);
/**
* 设置用户 ID
*
* @param value
*/
public static void setUserId(Object value) {
LOGIN_USER_CONTEXT_THREAD_LOCAL.get().put(GlobalConstants.USER_ID, value);
}
/**
* 获取用户 ID
* @return
*/
public static Long getUserId() {
Object value = LOGIN_USER_CONTEXT_THREAD_LOCAL.get().get(GlobalConstants.USER_ID);
if (Objects.isNull(value)) {
return null;
}
return Long.valueOf(value.toString());
}
/**
* 删除 ThreadLocal
*/
public static void remove() {
LOGIN_USER_CONTEXT_THREAD_LOCAL.remove();
}
}
在同服务下新建一个 HeaderUserId2ContextFilter :
package com.kitten.auth.filter;
import ...
@Component
@Slf4j
public class HeaderUserId2ContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 从请求头中获取用户 ID
String userId = request.getHeader(GlobalConstants.USER_ID);
// 判断请求头中是否存在用户 ID
if (StringUtils.isBlank(userId)) {
// 若为空,则直接放行
chain.doFilter(request, response);
return;
}
log.info("===== 设置 userId 到 ThreadLocal 中, 用户 ID: {}", userId);
LoginUserContextHolder.setUserId(userId);
try {
chain.doFilter(request, response);
} finally {
// 一定要删除 ThreadLocal ,防止内存泄露
LoginUserContextHolder.remove();
log.info("===== 删除 ThreadLocal, userId: {}", userId);
}
}
}
HeaderUserId2ContextFilter继承自OncePerRequestFilter,确保每个请求只会执行一次过滤操;request.getHeader(GlobalConstants.USER_ID);: 从请求头中获取用户 ID(userId);- 打印一行日志,看看等会测试的时候,能否拿的到请求头中的用户 ID;
chain.doFilter(request, response);: 将请求和响应传递给过滤链中的下一个过滤器。如果没有下一个过滤器,则请求会到达控制器。
这样就可以很方便的获取请求头传递的数据:
@PostMapping("/login")
public Response> userLogin() {
Long userId = LoginUserContextHolder.getUserId();
log.info("===> 请求头的userId: {}", userId)
...
}
ThreadLocal 的使用场景就是当每个线程只想操作属于自己的变量的时候, 并且别的线程也访问不到。
介绍完如何使用, 下面来说说常见问题
核心原理
ThreadLocal 的原理可以概括为:以线程为 Key,以存储的变量为 Value 的一个特殊 Map。
当我们创建一个 ThreadLocal 对象并调用 set 方法时,其实是在当前线程中初始化了一个 ThreadLocalMap。
// 在 java.lang.Thread 类中
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 是 ThreadLocal 的一个静态内部类,它实现了一个自定义的 Map 结构。这个 Map 的 Key 是 ThreadLocal 对象本身(的弱引用),Value 是你存入的变量值。

再来说下工作流程,调用set()方法 时,会将 value 存入 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); // 第一次调用时创建 Map
}
}
// 创建 ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 获取当前正在执行的线程
Thread.currentThread()。- 获取该线程的
ThreadLocalMap对象。- 如果
Map不为空,则以this(即当前myThreadLocal对象) 为 Key,以你要存储的value为 Value,存入 Map。- 如果
Map为空,则先为当前线程创建这个Map。
调用get()方法时,会从 map 中获取以当前 ThreadLocal 为 key 的 Entry
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
if (map != null) {
// 从 map 中获取以当前 ThreadLocal 为 key 的 Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// Map 不存在或找不到 Entry,调用初始化方法
return setInitialValue();
}
- 获取当前正在执行的线程
Thread.currentThread()。- 获取该线程的
ThreadLocalMap对象。- 如果
Map不为空,则以this(即当前myThreadLocal对象) 为 Key,在 Map 中查找对应的 Entry,并返回其 Value。- 如果 Map 为空或找不到对应的 Entry,则调用
initialValue方法来初始化一个值并返回(默认返回null,可通过重写initialValue来设置默认值)。
当我们使用 ThreadLocal时, 创建的过程为:
- 首先在应用中, 我们创建两个 ThreadLocal
ThreadLocal<Long> userIdThreadLocal = new ThreadLocal<>(); ThreadLocal<Long> connectionThreadLocal = new ThreadLocal<>(); - 假设有两个线程在工作, 那么对应的情况是:
Thread-1 ──────> ThreadLocalMap-1
├─ Key: userIdThreadLocal (弱引用) ──> Value: userId_1
└─ Key: connectionThreadLocal (弱引用) ─> Value: Connection_1
Thread-2 ──────> ThreadLocalMap-2
├─ Key: userIdThreadLocal (弱引用) ──> Value: userId_2
└─ Key: connectionThreadLocal (弱引用) ─> Value: Connection_2
从最开始就强调了一个概念 "弱引用", 这是啥?
Java 设计了不同强度的引用,主要是为了更灵活地控制对象的生命周期,方便 JVM 进行垃圾回收。
从强到弱大致分为四种:
- 强引用 (Strong Reference)
- 软引用 (Soft Reference)
- 弱引用 (Weak Reference)
- 虚引用 (Phantom Reference)
弱引用是用 java.lang.ref.WeakReference 类实现的引用。
- 被弱引用关联的对象只能生存到下一次垃圾收集发生之前
- 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
那么 ThreadLocal使用了 WeakReference, 就是用于保证资源可以被释放,减少内存泄露。
内存泄露
线程被复用的场景下,ThreadLocal有内存泄露的风险。
前面讲到 ThreadLocalMap,本质上是一个 Entry[] 数组, 一个 Entry通过弱引用关联当前 ThreadLocal对象, 通过强引用 Value保存设置的值。

当ThreadLocal 作为成员变量的时候, 随着方法执行完毕, 相应的栈帧也出栈了, 那么 ThreadLocal -> ThreadLocal对象 这条强引用链也就没了(排除线程复用场景, 如定义线程池), 那么, 没有别的栈对ThreadLocal引用的话, ThreadLocal对象对应的Entry将无法被访问到, 此時value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。
当ThreadLocal 作为静态变量的时候, 有静态变量这条强引用存在, ThreadlocalMap中的Key弱引用不会被GC回收, 正常情况下不会出现内存泄露, 但是当类加载器被卸载时, 对应的类会被回收, 那么这个强引用就没了
出现内存泄露的核心的点就是: 一个线程一直在运行,并且 value 一直指向某个强引用对象。
怎么解决呢?手动remove() 即可。
那为啥把Key 设置为弱引用? 反正都会内存泄露。其实, 它就是为了减少内存泄露的发生, 而且明显的当内存不足时, JVM能及时回收掉弱引用对象。
一旦 key 被回收,ThreadLocalMap 在进行 set、get 的时候就会对 key 为 null 的 Entry 进行清理(部分清理)。
set()
private 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)]) {
if (e.refersTo(key)) {
e.value = value;
return;
}
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
replaceStaleEntry()
private void replaceStaleEntry(ThreadLocal> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.refersTo(null))
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (e.refersTo(key)) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (e.refersTo(null) && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
这种设计产生的内存泄露类型是明确的: key==null 的 entry, 而且有明确的解决方案remove。如果是强引用,那么threadlocal永远被 threadlocalMap引用即便在业务中不再使用, 尤其是在线程池这种线程长期存在的场景。
感谢大家的观看喵, 笔者还是小白一枚(/ω\), 哪里写的有问题欢迎联系笔者沟通 qq: 1948677720

Comments NOTHING