前言
业务开发中经常使用 ThreadLocal 来存储用户信息等线程私有对象… ThreadLocal 内部构造是什么样子的?为什么可以线程私有?常说的内存泄露又是怎么回事?
公众号:liuzhihangs ,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!
介绍
ThreadLocal 类提供了线程局部变量。和正常对象不同的是,每个线程都可以访问 get()、set() 方法,获取独属于自己的副本。 ThreadLocal 实例通常是类中的私有静态字段,并且其状态和线程关联。
每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例访问; 一个线程消失之后,所有的线程局部实例的副本都会被垃圾回收(除非存在对这些副本的其他引用)。
使用
有这么一种使用场景,收到 web 请求,先进行 token 验证,而这个 token,可以解析出用户 user 的信息。所以我这边一般是这样使用的:
- 自定义注解,
@CheckToken
, 标识该方法需要校验 token。
- 在
Interceptor
(拦截器)中检查,如果方法有 @CheckToken
注解则校验 token。
- 从Header中获取
Authorization
,请求第三方或者自己的逻辑校验 token ,并解析成 user。
- 将user放到
ThreadLocal
中。
- controller、service 在后续使用中, 如果需要 user 信息,可以直接从
ThreadLocal
中获取。
- 使用结束后进行remove。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| public class LocalUserUtils {
private static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>();
public static void set(User user) { USER_THREAD_LOCAL.set(user); }
public static User get() { return USER_THREAD_LOCAL.get(); }
public static void remove() { USER_THREAD_LOCAL.remove(); }
}
@CheckToken @PostMapping("/doXxx") public Result<Resp> doXxx(@RequestBody Req req) {
Resp resp = xxxService.doXxx(req);
return result.success(resp); }
@Component public class TokenInterceptor implements HandlerInterceptor {
@Override public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception { LocalUserUtils.remove(); }
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { boolean assignableFrom = handler.getClass().isAssignableFrom(HandlerMethod.class);
if (!assignableFrom) { return true; }
CheckToken checkToken = null; if (handler instanceof HandlerMethod) { checkToken = ((HandlerMethod) handler).getMethodAnnotation(CheckToken.class); }
if (checkToken == null) { return true; }
String authorization = request.getHeader("Authorization"); log.info("header authorization : {}", authorization); if (StringUtils.isBlank(authorization)) { log.error("从Header中获取Authorization失败"); throw CustomExceptionEnum.NOT_HAVE_TOKEN.throwCustomException(); }
User user = xxxUserService.checkAuthorization(authorization); LocalUserUtils.set(user);
return true; } }
@Override public Resp doXxx(Req req) {
User user = LocalUserUtils.get();
return resp; }
|
抛出问题
- 为什么可以线程私有?
- 为什么建议声明为静态?
- 为什么强制使用后必须remove?

图 | 阿里巴巴 - Java开发手册(截图)

图 | 阿里巴巴 - Java开发手册(截图)
源码分析
Thread
1 2 3 4 5 6 7 8 9 10
| public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
|
可以看出 Thread
对象中声明了 ThreadLocal.ThreadLocalMap
对象,每个线程都有自己的工作内存,每个线程都有自己的 ThreadLocal. ThreadLocalMap
对象,所以在线程之间是互相隔离
的。
ThreadLocal
ThreadLocal则是一个泛型类,同时提供 set()
、get()
、remove()
等静态
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
public T get() {...} public void set(T value) {...} public void remove() {...} static class ThreadLocalMap {...} }
|
set(T value)方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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 getMap(Thread t) { return t.threadLocals; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
|
1.Thread.currentThread()
先获取到当前线程。
2. 获取当前线程的 threadLocals
属性,即 ThreadLocalMap
。
3. 判断 Map 是否存在,存在则赋值,不存在则创建对象。
get()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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(); }
|
1.Thread.currentThread()
先获取到当前线程。
2. 获取当前线程的 threadLocals
属性,即 ThreadLocalMap
。
3. 判断 Map 不为空,根据当前 ThreadLocal
对象获取 ThreadLocalMap.Entry
节点, 从节点中获取 value。
4.ThreadLocalMap
为空或者 ThreadLocalMap.Entry
为空,则初始化 ThreadLocalMap 并返回。
remove()方法
1 2 3 4 5 6 7
| public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
|
阅读 set()
、get()
、remove()
的源码之后发现后面其实是操作的 ThreadLocalMap
, 主要还是操作的 ThreadLocalMap
的 set()
、getEntry()
、remove()
以及构造函数。下面看是看 ThreadLocalMap 的源码。
ThreadLocalMap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> { Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16;
private ThreadLocal.ThreadLocalMap.Entry[] table;
private int size = 0;
private int threshold; private void setThreshold(int len) { threshold = len * 2 / 3; } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {...}
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {...}
private void set(ThreadLocal<?> key, Object value) {...}
private void remove(ThreadLocal<?> key) {...} }
|
- Entry 继承了
WeakReference<ThreadLocal<?>
也就意味着, Entry 节点的 key 是弱引用
。
- Entry 对象的key弱引用,指向的是
ThreadLocal
对象。
- 线程对象执行完毕,线程对象内实例属性会被回收,此时线程内
ThreadLocal
对象的引用
被置为 null
,即 Entry 的 key
为 null
, key 会被垃圾回收。
- ThreadLocal 对象通常为私有静态变量, 生命周期不会至少不会随着线程技术而结束。
- ThreadLocal 对象存在,并且
Entry的 key == null && value != null
,这时就会造成内存泄漏。
- 强引用、软引用、弱引用、虚引用
1 2 3 4
| 强引用(StrongReference):最常见,直接 new Object(); 创建的即为强引用。当内存空间不足,Java虚拟机宁愿抛出 OOM,也不愿意随意回收具有强引用的对象来解决内存不足问题。 软引用(SoftReference):内存足够,垃圾回收器不会回收软引用对象;内存不足时,垃圾回收器会回收。 弱引用(WeakReference):垃圾回收器线程,发现就会回收。 虚引用(PhantomReference):任何时候都有可能被垃圾回收,必须引用队列联合使用。
|
- 内存泄露:
1 2
| 内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。 —— 维基百科
|
构造函数及hash计算
1 2 3 4 5 6 7 8 9 10 11 12
| ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
|
threadLocalHashCode 是 ThreadLocal 的静态属性,通过 nextHashCode 方法获取。
1 2 3 4 5 6 7 8 9 10
| private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
|
- 初始化数组,长度16。
- 计算 key 的 hashCode,对2的幂取模。
- 设置元素,元素数及扩容阈值。
hashCode 通过步长 0x61c88647 累加生成, 并且使用了 AtomicInteger,保证原子性。
set()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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)]) { 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(); }
|
- 获取循环 Entry 数组,获取 tab[i] 处的 e, e != null 继续循环
- 此时发现 e 的 key 不存在,并且不是 null (hash冲突了。)
- 那就通过 e = tab[i = nextIndex(i, len)]) 继续获取下一个 i,并获取新的 tab[i] 处的 e。
- 赋值替换值结束结束并返回。
- e == null 结束循环。
1 2 3 4 5 6 7 8 9 10
|
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }
|
- 这块利用环形设计,如果长度到达数组长度,则从开头开始继续查找。
- int i = key.threadLocalHashCode & (len-1); 求出索引,并不是从0开始的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
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.get() == null) slotToExpunge = i;
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e;
if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; }
if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; }
tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
|
- 这里存在三个属性 key, value,以及 staleSlot, staleSlot节点的 Entry != null 但是 k == null。
- 向前扫描获取到上一个 Entry != null 但是 k == null 的节点及其索引, 赋值给 slotToExpunge, 没有扫描到的话 slotToExpunge 还是等于 staleSlot。
- 向后扫描 Entry != null 的节点,因为在 set 方法中, 后面还有一段数组没有遍历。
- 发现 key 相等的Entry节点了, 直接赋值,然后清除其他 Entry != null 但是 k == null 的节点, 并返回。
- 没有找到key相等的节点,但是找到了下一个 Entry != null 但是 k == null, 且此时 slotToExpunge 未发生变化,还是指向 staleSlot, 则 i 赋值给 slotToExpunge。
- 向后扫描没有扫描到,则直接对当前节点(索引值为staleSlot)的节点的value设置为null,并指向新value。
- 结束之后发现 slotToExpunge 被改变了, 说明还有其他的要清除。
getEntry()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
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]; } return null; }
|
- hashcode 取模求数组索引。
- 索引处获取到 Entry 则直接返回。
- 获取不到或者获取到的 Entry key 不相等时,有可能是因为 hash 冲突,被放到别的地方, 调用 getEntryAfterMiss 方法。
- getEntryAfterMiss 方法中。
- e == null 返回null。
- e != null 判断key, key相等返回 Entry, key == null, 那就需要清除这个节点,然后继续按照
nextIndex(i, len)
方法找下一个节点。
remove()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private void remove(ThreadLocal<?> key) { 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.get() == key) { e.clear(); expungeStaleEntry(i); return; } } } public void clear() { this.referent = null; }
|
- hashcode 取模求数组索引。
- 循环查找数组,将当前 key 的 Entry 的引用,将 value 设置为 null, 后面会被垃圾回收掉。
总结
为什么可以线程私有?
ThreadLocal 的 get()、set()、remove()方法中都有 Thread t = Thread.currentThread();
操作的其实是本线程,获取本线程的ThreadLocalMap。
每个线程都有自己的 ThreadLocal,并且是将 value 存放在一个以 ThreadLocal 为 key 的 ThreadLocalMap 中的。所以线程间隔离。
为什么建议声明为静态?
Java开发手册已经给出说明,还有就是,如果 ThreadLocal 设置为非静态,那就是某个线程的实例类,这样的话就会失去了线程共享的本质属性。
为什么强制必须时候后remove()?
这块可以和内存泄露一块说明, 通过上面的 ThreadLocalMap
处关于弱引用的讲解已经说明会产生内存泄露。至于如何解决也给出了答案:
1.set()
时清除 Entry != null && key == null 的节点, 将其 value 设置为 null。
2.getEntry()
时清除当前 key 到 nextIndex(i, len)==null 之间的
Entry != null && key == null 的节点, 将其 value 设置为 null。
3.remove()
时清除指定key
的 Entry != null && key == null 的节点, 将其 value 设置为 null。
之所以使用remove(),还是为了解决内存泄露的问题。
Last
- 使用时注意声明为
private static final
。
- 使用后要
remove()
。