ThreadLocal 是一个用来处理高并发场景的重要工具。本文将从使用和原理两个方面来介绍ThreadLocal。

ThreadLocal

java.lang.ThreadLocal提供了线程内变量。通过ThreadLocal可以将一个变量与当前运行的线程绑定,只有通过该线程才能获取到对应的变量。

一、实践篇

ThreadLocal简单使用

此处不再介绍ThreadLocal的API,下面通过用户信息管理来展示ThreadLocal的使用。

在Web系统中,每个请求都在一个线程内完成。对于每个线程,可以使用ThreadLocal来管理当前用户信息。(每个用户请求对于其他用户是隔离的)

image-20210618222605706

上图中,当一个请求到达首先会经过UserInterceptor来获取用户信息,并将信息保存到UserContextHolder上下文中,之后在Controller、Service和Dao层都可以获取到UserContextHolder中保存的用户信息。

当多个用户访问Web系统的时候,每个请求都会对一个拿到线程进行处理,每个线程都有对应的ThreadLocal变量。由于变量是线程私有的,而线程与每个用户是一一对应的,因此最终变量属于用户私有的,不存在并发问题。

封装用户上下文对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UserContextHolder {

private static ThreadLocal<MemberRespVo> memberRespVoContext = new ThreadLocal<>();

// Set the user information if it not null.
public static void setCurrentUser(MemberRespVo memberRespVo) {
// ThreadLocal is thread-local variable, therefore it hasn't concurrent problem.
// Each thread call the threadLocal.get is get different result.
if (memberRespVoContext.get() == null) {
memberRespVoContext.set(memberRespVo);
}
}

// Get current user information.
public static MemberRespVo getCurrentUser() {
return memberRespVoContext.get();
}

// Remove current user information from ThreadLocal.
public static void removeCurrentUser() {
memberRespVoContext.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
public class LoginUserInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

MemberRespVo memberRespVo = (MemberRespVo) request.getSession().getAttribute(PlatformAuthConstant.LOGIN_USER);
if (memberRespVo == null) {
// hasn't login, redirect to login page.
response.sendRedirect("http://auth.platform.com/login.html");
return false;
} else {
UserContextHolder.setCurrentUser(memberRespVo);
return true;
}
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// threadLocal.remove();
}


/**
* Remove ther user infomation in ThreadLocal when complete of request processing and view rendering.
* Why not remove in postHandle ?
* https://stackoverflow.com/questions/37358426/remove-threadlocal-object-within-a-spring-mvc-website
* The postHandle method looks like a good place, but afterCompletion is better because it's called
* even when the handler fails to process the request correctly (aka an exception occurred).
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContextHolder.removeCurrentUser();
}
}

此处使用的时候需要注意,由于项目中普遍使用ThreadPool,一个用户可能会在不同时间获取到其他用户获得过的线程。如果这个线程中对应的ThreadLocal没有被清除,那么可能会造成不正确的结果。因此必须要在线程使用完毕之后,对线程的ThreadLocal进行清除,确保逻辑的正确性。

对ThreadPool中获取的线程可以采用两种方式进行清除:

  • 在Interceptor中清除
  • 继承ThreadPoolExecutor,在其中的钩子函数中(beforeExecute、afterExecute)进行清除

对于第一种方式,已经在代码中进行展示,下面展示了第二种方式:

1
2
3
4
5
6
public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor {
@Override
protected void afterExecute(Runnable r, Throwable t) {
// Call remove on each ThreadLocal
}
}

ThreadPool在Spring运用

Spring中的org.springframework.web.context.request.RequestContextHolder类中使用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
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
public abstract class RequestContextHolder  {

private static final boolean jsfPresent =
ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");


/**
* Reset the RequestAttributes for the current thread.
*/
public static void resetRequestAttributes() {
requestAttributesHolder.remove();
inheritableRequestAttributesHolder.remove();
}

/**
* Bind the given RequestAttributes to the current thread,
*/
public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
setRequestAttributes(attributes, false);
}

/**
* Bind the given RequestAttributes to the current thread.
*/
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
}
else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}

/**
* Return the RequestAttributes currently bound to the thread.
*/
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}

/**
* Return the RequestAttributes currently bound to the thread.
*/
public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
RequestAttributes attributes = getRequestAttributes();
if (attributes == null) {
if (jsfPresent) {
attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
}
if (attributes == null) {
throw new IllegalStateException("No thread-bound request found...");
}
}
return attributes;
}
// ...
}

需要注意,Spring使用了NamedThreadLocal和NamedInheritableThreadLocal两个类来分别保存请求的属性以及内容。对于内容使用NamedInheritableThreadLocal可以运行在当前线程创建的子线程可以获取,而NamedThreadLocal只允许在当前线程中获取。

阿里巴巴开发手中的ThreadLocal

使用ThreadLocal实现SimpleDateFormat线程安全使用

image-20210619093415554

image-20210619093516563

ThreadLocal的使用注意

image-20210619093721402

ThreadLocal使用注意

image-20210619093923364

二、原理篇

ThreadLocal的一些重要属性

首先,先观察ThreadLocal中重要的属性:

1
2
3
4
5
6
7
8
9
10
// 当前ThreadLocal的hashCode,通过 nextHashCode()运算得到,被用来计算当前ThreadLocal在ThreadLocalMap中的位置
private final int threadLocalHashCode = nextHashCode();
// 哈希魔数,主要与斐波那契哈希和黄金分割有关
private static final int HASH_INCREMENT = 0x61c88647;
// 返回计算得到的下一个哈希值, i * HASH_INCREMENT, i 代表调用的次数
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 确保同一台机器中的不同ThreadLocal的threadLocalHashCode具有唯一性
private static AtomicInteger nextHashCode = new AtomicInteger();
  • HASH_INCREMENT并不是随意给定的,将其转换为十进制为16405315272654435769,等于(√ 5-1) / 2 乘以2的32次幂,(√ 5-1) / 2 是黄金分割数,约为 0.618。这就意味着 0x61c88647 可以理解为黄金分割数乘以2的32次幂。它能确保通过nextHashCode 生成的hash值在2的幂上均匀分布,并且小于2的32次幂。

  • 下面是Java专家对这个值的介绍

    1
    This number represents the golden ratio (sqrt(5)-1) times two to the power of 31 ((sqrt(5)-1) * (2^31)). The result is then a golden number, either 2654435769 or -1640531527.

ThreadLocalMap

接下来观察ThreadLocalMap

处理上面提到的属性,还有一个重要的属性,那就是ThreadLocalMap。ThreadLocalMap是ThreadLocal的静态内部类。当一个线程拥有多个ThreadLocal,需用一个容器来管理它们。ThreadLocalMap的功能就是用来管理同一个线程中的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
static class ThreadLocalMap {
/**
* Storage structure of key value to entity
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
// value与当前线程对应,不被弱引用追踪
Object value;

/**
* 键值对构造器
* key,ThreadLocal中的key被包装为弱引用
* v,值
*/
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

// 初始容量,一定为2的n次幂
private static final int INITIAL_CAPACITY = 16;

// 保存实体键值对的数组,长度一定为2的次幂
private Entry[] table;

// ThreadLocalMap中元素个数
private int size = 0;

// 扩展临界值, 默认为数组长度的2/3
private int threshold;
}

从上面可以看出ThreadLocalMap实际上是一个简单的Map结构,底层是一个数组。数组中元素类型为Entry,Entry中的key是ThreadLocal的引用,value是ThreadLocal中的值。ThreadLocal使用线性探索来解决哈希冲突,如果发生冲突,继续查找下一个非空位置。

这样一来,将有可能发送内存泄漏。

ThreadLocal内存泄漏

当ThreadLocal没有外部的强引用,当GC发生是它将会被回收。ThreadLocalMap中保存的key将会变成null,Entry被threadMapLocal对象引用,threadMapLocal被Thread对象引用。如果Thread一直不被中断(如在线程池中维护),value的对象将会一直保存在内存中,直到线程被销毁。

为了避免内存泄漏,在使用完毕ThreadLocal变量之后,需要将其中对应的Entry进行手动移除,这样一来可以保证Entry中的value可以被回收。

1
2
3
4
5
6
// 清理与当前ThreadLocal保存的的键值对
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
  • remove实现获得当前线程Thread持有的ThreadLocalMap,调用ThreadLocalMap的remove方法删除Map中与当前ThreadLocal关联的键值对。这样一来在GC发生时就可以将Entry中强引用value进行回收了。

下面探究ThreadLocal如何实现线程隔离。

ThreadLocal的set方法

源码如下:

1
2
3
4
5
6
7
8
public void set(T value) {      // 设置一个值
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 通过线程获取到线程绑定的ThreadLocalMap
if (map != null) // map可能未被创建
map.set(this, value); // 直接设置
else
createMap(t, value); // 将其创建并将当前值设置进去
}

set的主要步骤如下:

  • 实现获取当前线程的引用
  • 通过当前线程的引用获取线程内部的ThreadLocalMap
  • 如果ThreadLocalMap为空,创建一个ThreadLocalMap,并设置
  • 如果ThreadLocalMap不为空,将其添加到map中

set的时序图:

img

下面展示了getMap()方法:

1
2
3
ThreadLocalMap getMap(Thread t) {   // 获取线程的threadLocals属性(ThreadLocalMap)
return t.threadLocals;
}

getMap方法主要是获取到指定线程的threadLocals属性,即:

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到每个Thread中都有一个threadLocals属性,默认为null,因此threadLocals是线程隔离的,其中保存的值自然也是线程隔离的。

当ThreadLocalMap的set方法被调用,当前的ThreadLocal对象的引用将作为key,需要保存的对象作为value,封装为Entry保存到map中。

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
// 将键值对保存到map中
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)]) {
// 获取元素的hash值
ThreadLocal<?> k = e.get();

// 如果ThreadLocal的key匹配,直接改变map中的值
if (k == key) {
e.value = value;
return;
}

// 如果key为null,表明key被清除,可以直接替换
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

// 在遇到空插槽之前,找不到匹配的ThreadLocal对象。将ThreadLocal对象和缓存的值排列在空插槽中
tab[i] = new Entry(key, value);
int sz = ++size;
// 如果没有元素被清理,检查当前数组的容量是否差错扩展临界值,以此来考虑是否需要扩展。
if (!cleanSomeSlots(i, sz) && sz >= threshold) {
rehash();
}
}

下图展示了Thread、ThreadLocal、ThreadLocalMap三者之间的关系:

img

ThreadLocal的get方法

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 尝试从map中获取Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 如果map为null,初始化当前线程的ThreadLocalMap,最后返回与当前ThreadLocal对象关联的初始值
return setInitialValue();
}

执行的时序图如下:

img

ThreadLocalMap.getEntry方法:

1
2
3
4
5
6
7
8
9
10
11
12
private Entry getEntry(ThreadLocal<?> key) {
// 运算获取下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果e不为空,而且e对一个拿到key与传入的相同,表示正是要找的,直接返回
if (e != null && e.get() == key) {
return e;
} else {
// 如果key不相等或者e为null,从i开始先后探索查找
return getEntryAfterMiss(key, i, e);
}
}

下面展示的是ThreadLocalMap的resize方法:

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
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
// 创建一个新的数组,长度为原来的2倍
Entry[] newTab = new Entry[newLen];
int count = 0;

// 从旧数组中将值复制到新数组
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// 如果发现一个k以及为null,即为垃圾值,直接将value的引用就行清除,以便其可以被GC回收
if (k == null) {
e.value = null;
} else {
// 计算ThreadLocal在Map的新位置
int h = k.threadLocalHashCode & (newLen - 1);
// 如果发生了冲突,使用线性探索寻找合适的位置
while (newTab[h] != null) {
h = nextIndex(h, newLen);
}
newTab[h] = e;
count++;
}
}
}
// 设置新的size,临界长度
setThreshold(newLen);
size = count;
table = newTab;
}

三、参考文献

评论