1.简介
ThreadLocal用于存储线程的局部变量,通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。ThreadLocal诞生于JDK 1.2,直到JDK5.0开始支持范型。
2.应用场景
ThreadLocal的应用场景比较广泛,例如web项目中登陆信息的存储、IOC中Request作用域的实现、Spring事物管理的实现、线程同步工具Exchanger的实现等。除了登录信息的存储以外,其他的多多少少都是涉及到底层的源码,写出来篇幅太大,所以下面简单讲述一下如何使用ThreadLocal存储登陆信息。
一般浏览器发送http请求到后端服务器,都会将用户信息的token以cookie或header形式携带过去,并在后端拦截器中对token校验是否合法。如果请求的接口中需要用到一次或多次用户信息(比如ID、名称、生日、职级等)进行业务处理,这就需要将用户信息从controller一层一层作为参数传递下去,这无疑增加了代码的复杂程度,重点是不够优雅,使用ThreadLocal完全可以避免这个问题。
1.用户信息对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Data @ToString public class UserInfo {
private Long id;
private Long deptId;
private String name;
private String phone;
private String email;
private String birthday; }
|
2.创建一个ThreadLocal封装类,内部定义一个私有ThreadLocal并对外提供get、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
| public class UserInfoThreadLocal {
private static final ThreadLocal<UserInfo> threadLocal = new ThreadLocal<>();
public static void set(UserInfo value){ threadLocal.set(value); }
public static UserInfo get(){ return threadLocal.get(); }
public static void remove(){ threadLocal.remove(); }
public static UserInfo getAndValidate(){
UserInfo userInfo = get(); if(userInfo == null){ }
return userInfo; } }
|
3.拦截器负责校验信息,并将合法的用户信息注册到ThreadLocal中:
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
| @Component public class LoginInfoInterceptor implements HandlerInterceptor {
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token"); if(StringUtils.isEmpty(token)){ }
UserInfo userInfo = new UserInfo(); userInfo.setId(323L); userInfo.setDeptId(542L); userInfo.setPhone("110"); userInfo.setEmail("110@163.com"); userInfo.setBirthday("1995-12-20");
UserInfoThreadLocal.set(userInfo);
return true; }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserInfoThreadLocal.remove(); } }
|
4.web项目的controller层,获取并打印拦截器查询并校验通过的用户信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @RestController public class TestController {
@Autowired private TestService testService;
@RequestMapping("/test") public void test(){
UserInfo userInfo = UserInfoThreadLocal.getAndValidate(); System.out.println("controller:" + JSONObject.toJSONString(userInfo));
testService.testQuery(); } }
|
5.web项目的service层,获取并打印拦截器查询并校验通过的用户信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Service public class TestServiceImpl implements TestService {
@Override public void testQuery() {
UserInfo userInfo = UserInfoThreadLocal.getAndValidate(); System.out.println("testQuery:" + JSONObject.toJSONString(userInfo));
testMethod(); }
private void testMethod(){
UserInfo userInfo = UserInfoThreadLocal.getAndValidate(); System.out.println("testMethod:" + JSONObject.toJSONString(userInfo)); } }
|
打印结果:
controller:{“birthday”:”1995-12-20”,”deptId”:542,”email”:”110@163.com“,”id”:323,”phone”:”110”}
testQuery:{“birthday”:”1995-12-20”,”deptId”:542,”email”:”110@163.com“,”id”:323,”phone”:”110”}
testMethod:{“birthday”:”1995-12-20”,”deptId”:542,”email”:”110@163.com“,”id”:323,”phone”:”110”}
从上面的Demo代码可以看出来,只要在拦截器层面对token的验证通过,并将用户信息存储在创建的ThreadLocal对象中,就可以在任何逻辑层、任何方法直接获取用户信息,提高了代码的简洁程度。
3.存储原理
Thread类中有个成员变量threadLocals,这个变量的引用类型是ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现Map接口,本质上是一个table数组,数组中每个元素都是K-V键值对组成的Entry对象,其中K就是ThreadLocal实例,V就是要存储的局部变量对象。

4.源码解析
4.1 hash算法
ThreadLocalMap的存储逻辑和HashMap有一些相似的地方,内部都是维护一个Entry类型数组,然后通过对key的哈希码进行位与运算,定位出存储的数组坐标。ThreadLocal作为key提供的哈希码查询方法并非hashCode(),而是通过成员变量threadLocalHashCode去表达。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class ThreadLocal<T> { 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); } }
|
存储哈希码的成员变量threadLocalHashCode被final修饰,也就意味着实例被创建时内部threadLocalHashCode固定为nextHashCode()方法返回的值。这个方法很有意思,每次调用都是将nextHashCode递增0x61c88647,并返回递增后的值。按照这个设计逻辑,每次创建的ThreadLocal实例的哈希值都是不同的,都会比上一次的哈希值高0x61c88647,并且考虑到会被多个线程创建,使用AtomicInteger维护递增值确保线程安全。
因此线程的threadLocals值必须由同一个实例进行存取,这样才能定位到同一个数组下坐标,这也是上述的例子中把ThreadLocal设计成static、final的原因。
4.2 set()源码
ThreadLocal的set方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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为null,调用createMap()初始化:
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
|
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
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); }
|
如果Map已经被初始化,调用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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| 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是过期元素,那么这个Entry也没有任何存在的意义,因为没有任何途径能拿到此Entry的value。这时候就需要调用replaceStaleEntry()方法将此Entry替换掉,将此数组坐标重新利用起来:
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
| 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); }
|
上面对数组中Entry的检查中不难发现,无论往左还是往右一旦碰到为null的元素检查就停止了,这会漏掉一部分Entry的检查。因此在每次清理的时候,顺带将不为空的元素左移,挤出所有值为null的下坐标,确保所有不为null的Entry连续挨在一起。这些逻辑都在expungeStaleEntry方法中实现:
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
| private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length;
tab[staleSlot].value = null; tab[staleSlot] = null;
size--;
Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null) h = nextIndex(h, len);
tab[h] = e; } } }
return i; }
|
cleanSomeSlots主要用于控制扫描,检查还存不存在有问题的元素,如果有就清理掉。扫描的趟数为log2(数组长度),执行过程中一旦发现了过期元素,扫描趟数会重置,并且返回值变为true:
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 boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length;
do {
i = nextIndex(i, len); Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i); }
} while ( (n >>>= 1) != 0);
return removed; }
|
数组扩容:
1 2 3 4 5 6 7 8 9
| private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4) resize(); }
|
整个set方法的执行过程分为俩部分,一部分是通过哈希值与数组长度的取余运算,定位到要存储的坐标,并解决坐标冲突的问题。另一部分也是最复杂的一部分,主要涉及到对过期元素的清理工作并防止内存泄漏,因为Entry对象中key的引用是弱引用,所指向的实例如果没有其他强引用指向,随时可能被回收掉。
- 通过实例的哈希值(threadLocalHashCode属性),定位到要存储的坐标。
- 计算出的坐标可能会冲突(类似hashMap的哈希碰撞),这时需要对比已存储的Entry的key与要存储的key是否一致。
- 如果key地址相同,直接覆盖value值,结束。
- 如果key为null(过期元素),将要set的值包装成Entry覆盖当前坐标,然后进行扫描并清除所有过期元素
- 如果计算出来的坐标不存在冲突,直接插入此坐标
- 如果达到扩容的阈值,对数组进行扩容
4.3 get()源码
ThreadLocal的get方法:
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
| 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(); }
|
ThreadLocalMap的getEntry方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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); }
|
如果通过哈希值寻找Entry失败,也就是出现了坐标冲突,那就要往后遍历寻找:
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
| 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; }
|
setInitialValue方法没啥逻辑,就是初始化线程的threadLocals:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) map.set(this, value); else createMap(t, value); return value; }
|
get方法相对于set方法而言要简单很多,无非就是根据哈希值计算数组坐标,顺带也会清理过期元素。
4.4 remove()源码
ThreadLocal的remove()方法:
1 2 3 4 5 6 7 8
| public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
|
ThreadLocalMap的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
| 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; } } }
|
remove方法的作用不仅仅是清除指定的key,还会调用expungeStaleEntry()方法将清除的坐标继续向右遍历,清除遇到的所有过期元素,并且将null的坐标剔除掉。
5.内存泄漏
使用ThreadLocal可能出现的内存泄漏,是指value使用完毕后并没有被及时的清理掉。通俗点讲,我们希望线程执行完某个步骤后(value后续不会被使用)、生命周期结束前;或者线程执行完某个步骤后、归还线程池前,或者线程生命周期结束后;就该被清除但没被清除,都可以定义为内存泄漏。
从乐观的角度看,ThreadLocal导致的内存泄漏并不一定导致内存溢出,也许value只是在内存中多逗留了一会。
5.1 弱引用
每个线程内部会维护一个ThreadLocalMap,用于存储当前线程的私有变量,ThreadLocalMap内部所有Entry都是以ThreadLocal对象作为key,并且Entry的key对ThreadLocal是弱引用,但是对value是强引用。如图:

解决内存泄漏最直接有效的办法,就是保持使用完毕后调用remove()方法的习惯,但ThreadLocal的设计者为了避免开发者忘记调用remove()方法,提供了弱引用机制尽可能的避免内存泄漏。之所以这么说,是因为弱引用并不能百分百的避免内存泄漏,只能解决部分特定场景。
从上面的源码可以看出,每次调用get()、set()方法时,会去检查所有key为null的Entry并清除。也就是说,即使没有调用remove()方法,对应的ThreadLocal对象变成null,线程后续用到其他ThreadLocal对象的get()、set()方法时,依然可以自动帮你清理(不用白搭)。
我们平时使用ThreadLocal对象,要么设置为static全局共享,要么每次都new一个新的;在使用线程方面,要么每次创建新的线程,要么将线程池化。组合起来就是四种场景,到底哪些场景JDK可以帮我们避免,哪些需要我们开发时注意的,得一个个单独分析。
5.2 非静态化与普通线程
ThreadLocal每次使用的时候,都是new出一个新对象,线程每次也都是创建新对象。这种场景比较少见,也比较奇葩,因为ThreadLocal设计的意图,就是让value可以在任何地方获取到,这就代表任何需要value的方法,都得将ThreadLocal作为参数传进去,为啥不直接传value呢。
另外,当线程执行完某个方法,后续不再需要value时,方法出栈后ThreadLocal对象就仅剩Entry中的key弱引用形式指向它,肯定会被回收。这时Entry中的key就变成了null,如果线程后续使用了其他ThreadLocal对象的get()、set()方法,之前的value就会被自动回收。
如果Entry的key不是弱引用,key对于ThreadLocal对象是强引用,即使出栈也不会被回收,在不调用remove()方法的前提下,也就无法尽可能提前回收value。但就算没有提前回收,线程生命周期结束,ThreadLocal与线程对象都会被回收,对应的value也就GC不可达,最终还是会被回收。
5.3 非静态化与线程池
ThreadLocal每次使用的时候,都是new出一个新对象,处理任务的线程则是从线程池中获取。由于线程不会被销毁,并且会不停的处理任务,在开发者忘记调用remove()方法的前提下,会不停的往自身的ThreadLocalMap中添加value。由于ThreadLocal每次都是new出来的,弱引用设计的存在保证能被及时回收,当线程下次执行任务并用到ThreadLocal时,能自动的将上次任务用到的value清除掉。
弱引用在此场景的作用,除了尽可能的提前回收value外,还可以避免内存溢出。假设没有弱引用,线程归还线程池时,value在ThreadLocalMap中依然存在。即使下次执行任务并调用get()、set()方法,由于上次设置的Entry的key不为null,依然无法将其清除,并且value只增不减,时间久了必定导致内存溢出。
5.4 静态化与普通线程
将ThreadLocal设置为静态变量或常量,是目前比较常见的使用方式。这意味着ThreadLocal对象被class对象强引用,永远不可能被删除,弱引用也就发挥不了任何作用,开发者如果忘记调用remove()方法,是无法进行提前回收的,只能等线程生命周期结束后被GC回收。
5.5 静态化与线程池
这种场景在实际开发中使用最多,由于ThreadLocal对象被class对象强引用,永远不可能被删除,线程也不会被销毁,这就要求开发者每次使用完必须手动调用remove()方法,否则value会永远停留在线程的ThreadLocalMap中,这就有几率造成内存溢出。
为什么说有几率而不是百分百造成内存溢出呢?比如上面提到的用户登陆信息存储,虽然springMVC使用线程池来处理http请求,但好在每次进入拦截器都会重新设置value,上次请求产生的value对象在Entry中被覆盖,仅有的一个指向它的引用也没了,自然会被回收。
总结
线程的局部变量是存储在线程自身的ThreadLocalMap中,ThreadLocalMap与HashMap都属于键值对映射结构,不过ThreadLocalMap采用数组线性探测的方式存储数据,HashMap使用数组+链表存储数据。
而ThreadLocal对象仅仅只是个方便存取数据的快捷键而已,一般都是创建静态变量并提供静态存取方法,使用起来比较方便。另外在使用完毕后尽量使用remove()方法进行清理,方便GC尽快回收。