ThreadLocal是什么鬼?

ThreadLocal是什么鬼?

薛定谔的汪

前言

之前写过一篇关于[SimpleDateFormat线程安全问题](link: http://zhengyk.cn/2017/10/31/java/Thread_sdf/)的记录博客,里面提到了使用 ThreadLocal 来保证 SimpleDateFormat 线程安全,这是一种比在每个方法里都 new SimpleDateFormat更优雅的方式,当然现在操作时间推荐使用 joda-time ,joda-time线程安全且功能丰富、性能高。

其实网上介绍 ThreadLocal的文章也是很多很多,但我觉得自己还是得总结下来,一是记录自己的理解,二是方便以后查漏补缺。

那么ThreadLocal 是什么呢?

ThreadLocal

ThreadLocal官话解释是线程本地存储,它为变量在每个线程中都创建了一个副本,那么每个线程访问自己的变量副本,自然实现了变量的线程隔离。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ThreadLocalTest {

private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL_A = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
};

private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL_B = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd HH:mm:ss");
}
};

}

结合 ThreadLocal 和 Thread 的源码,草画了一张ThreadLocal 的结构图:

每个Thread内有一个 ThreadLocalMap,这个 ThreadLocalMap 的 每个 key 就是 每个ThreadLocal 对象(THREAD_LOCAL_A、THREAD_LOCAL_B),value 是变量副本。

1
2
/** Thread类中的ThreadLocalMap */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 是 ThreadLocal 里的静态内部类,ThreadLocalMap由 ThreadLocal 来操作。

这样,每个线程都有属于自己的变量副本,实现了变量线程隔离。

一些常用方法

void set(T value)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

//创建 ThreadLocalMap
void createMap(Thread t, T firstValue) {
//将 ThreadLocal对象当做key
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

T 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
26
27
28
29
30
31
32
33
public T get() {
Thread t = Thread.currentThread();
//同样是获取当前线程的map
ThreadLocalMap map = getMap(t);
if (map != null) {
//ThreadLocalMap 的键就是此 ThreadLocal,根据键找到 Entry,最终获取 value 返回
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
//map 为 null则去获得初始值
return setInitialValue();
}

private T setInitialValue() {
//没重写initialValue默认返回null,使用时最好判断下是否null,如果我们重写此方法返回非null就不用判断了
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//获取初始值的时候判断当前线程的 ThreadLocalMap 是否为 null,是null的话还是要初始化Map
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

//给 ThreadLocal 设置变量,默认为null,这个方法是可以被重写,如上述示例代码
protected T initialValue() {
return null;
}

ThreadLocalMap

前面提到 ThreadLocalMap 是 ThreadLocal 的静态内部类,key 是 ThreadLocal对象本身,value 是变量副本

1
2
3
4
5
6
7
8
9
10
11
12
static class ThreadLocalMap {

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
....
}

可以看到 ThreadLocalMap使用 Entry 来存储 K-V,而且这个 Entry 的 key 是弱引用,下次 GC 时要被回收,那就可能存在内存泄露问题。

ThreadLocal存在的内存泄露问题:由于ThreadLocalMap 存在于 Thread 中,和线程的生命周期一样,但是 ThreadLocal 在类里,往往 ThreadLocal 的生命周期比ThreadLocalMap要长(ThreadLocal 声明为 static 后生命周期会更长),由于Entry 使用ThreadLocal的弱引用,当 ThreadLocal 没有外部的强引用引用它时,下次 GC ThreadLocal 弱引用会被回收,而 value 是强引用,所以如果当前线程一直不结束,那么ThreadLocalMap 中就会存在key 为 null的数据,造成内存泄露。

避免内存泄露:用完后及时调用 remove 方法。

ThreadLocal 实践

众所周知,可以利用Spring 的 AOP 来记录日志,有环绕通知、前置通知、后置通知、异常通知等,加入我想实现一个记录某个方法执行的时间,使用环绕通知是可以解决的,那如果使用前置通知和后置通知呢?

前置通知和后置通知是不在同一个方法里的,所以不能像环绕通知那样在执行方法前后记录时间。

这时候可以利用 ThreadLocal 来实现。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RecordTime {

private static final Logger logger = LoggerFactory.getLogger(RecordTime.class);

private static final ThreadLocal<Long> recordTime = new ThreadLocal<>();

@Before("within(com.*..*)")
public void addBeforeLogger(JoinPoint joinPoint) {
recordTime.set(System.currentTimeMillis());
}

@AfterReturning(returning = "rnt", pointcut = "within(com.*..*)")
public void addAfterReturningLogger(JoinPoint joinPoint, Object rnt) {
Long time = System.currentTimeMillis() - recordTime.get();
logger.info("耗时:"+ time +"毫秒\r\n");
//用完后及时remove掉,防止内存泄露
recordTime.remove();
}
}

ThreadLocal 在框架中的应用

在 Spring 框架中,很多地方使用到了 ThreadLocal,如 **RequestContextHolder **请求上下文类,当我们在 service 层用到 request 或 controller 时,可以直接从 controller 层传递request、response 到 service 层,但是这样的话方法参数变多,代码不简洁。

其实 Spring 提供的RequestContextHolder类,可以让我们直接在service 层中获取 request 和 response

代码实例:

1
2
3
4
5
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();

HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();

HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();

我们只要封装一个获取 request 和response 的方法抽取去 BaseService 中即可,这样使用更为优雅、简洁。

RequestContextHolder 就是利用 ThreadLocal 与当前请求的线程绑定来实现的:

1
2
3
4
5
6
public abstract class RequestContextHolder {
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
...
}
  • Title: ThreadLocal是什么鬼?
  • Author: 薛定谔的汪
  • Created at : 2018-09-01 18:01:54
  • Updated at : 2023-11-17 19:37:37
  • Link: https://www.zhengyk.cn/2018/09/01/java/ThreadLocal/
  • License: This work is licensed under CC BY-NC-SA 4.0.