搞懂ThreadLocal原理

一、关于

ThreadLocal不是一个线程,而是一个线程的本地化对象,当某个变量在使用ThreadLocal维护时,它会在每个线程中创建独立的一个副本,不同线程中对这个变量的修改不会影响到其他线程中这个变量的值。

ThreadLocal采用了空间换时间的思想,主要用来实现多线程环境下线程安全和保存线程上下文中的变量。

可以笼统的理解它为线程范围下的全局变量存储对象。

二、API

  • public void set(T value) {}

    将T类型的值设置到ThreadLocal中

  • public T get() {}

    获取ThreadLocal对象的值

  • public void remove() {}

    移除ThreadLocal中的值

    三、源码分析

    1.1set方法

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
  1. 首先获取到当前线程。
  2. 根据getMap方法根据当前线程获取到ThreadLocalMap 。我们看一下getMap方法:
1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

直接返回的是t.threadLocals,threadLocals是Thread类的一个参数,类型是ThreadLocal中的内部类ThreadLocalMap,初始值为null。

再看ThreadLocal类结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
...............
private Entry[] table;
...............
}

其内部维护了一个Entry数组,且注意Entry类是继承自WeakReference,弱引用对象,所以需要注意jvm回收引发的问题。

  1. 回到set方法
  • 如果获取出来的map不为空,就将当前ThreadLocal对象和value值放进去。
  • 如果map为空,就创建map
1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

1.2get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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. 获取当前线程。
  2. 获取当前线程的所有ThreadLocal对象(ThradLocalMap)。
  3. 如果map不为空,就根据ThreadLocal对象获取到存储在其中的值。
  4. 如果map为空,则初始化将当前ThreadLocal对象中的值设为null,并放入ThreadLocalMap。

    1.3remove方法

1
2
3
4
5
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

获取当前线程的所有ThreadLocal对象,并将当前对象从ThreadLocalMap 中移除。

这里需要着重注意,不然会引起jvm内存泄漏!

上面说到了Entry是继承WeakReference的对象就是弱引用对象,而调用构造方法时,也说明了Entry的key是指向当前ThreadLocal的弱引用对象。


弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

如果Entry的key被回收了,那么其对应的值就不会被remove掉了。所以在使用完之后需要手动remove()。

四、使用示例

这里我们来模拟将用户登录信息存放到ThreadLocal中,项目为SpringBoot项目,结构为:

缓存工具类CacheUtil,此类用于提供对ThreadLocal增删查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.yl.demo.util;

public class CacheUtil {

private static ThreadLocal<String> store = new ThreadLocal<String>();

public static String getStore() {
return store.get();
}

public static void setStore(String value) {
store.set(value);
}

public static void remove() {
store.remove();
}
}

拦截器TestInterceptor,拦截所有请求,在进入控制器之前,在ThreadLocal中设置信息,在执行完控制器的方法后,移除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
package com.yl.demo.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.yl.demo.util.CacheUtil;

public class TestInterceptor implements HandlerInterceptor{

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println(Thread.currentThread().getName());
CacheUtil.setStore("这里面是用户登录信息");
return true;
}

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

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
CacheUtil.remove();
}

}

mvc配置类,让拦截器拦截所有路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.yl.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.yl.demo.interceptor.TestInterceptor;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

@Bean
public TestInterceptor testInterceptor() {
return new TestInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(testInterceptor()).addPathPatterns("/**");
}

}

测试Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.yl.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.yl.demo.util.CacheUtil;

@RestController
public class TestController {

@GetMapping("/get")
public String doGet() {
return Thread.currentThread().getName()+"-get : " + CacheUtil.getStore();
}
}

运行项目,在浏览器测试:



可以看到,同一线程下都可以获取到ThreadLocal中的值。

五、子线程获取父线程中ThreadLocal的值

我们这里改造一下接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.yl.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.yl.demo.util.CacheUtil;

@RestController
public class TestController {

@GetMapping("/get")
public String doGet() {
// 新起一个线程获取值
new Thread(()-> {
System.out.println(Thread.currentThread().getName()+"-get : " + CacheUtil.getStore());
}).start();
return Thread.currentThread().getName()+"-get : " + CacheUtil.getStore();
}
}

new Thread(()-> {}).start()这种写法是java8中的写法,如果jdk低于1.8,就会报错,那么可以根据自己的实际情况来写,这里实现的就是新启动一个线程来获取存储在ThreadLocal中的值,看能否取到。



我们可以看到,新线程为什么取到的是null呢?

因为ThreadLocal是线程本地变量,设置值的时候是http-nio-8080-exec-1这个线程设置的,对其他线程没有影响,所以Thread-103-get线程是取不到值的。

在这个方法中,线程Thread-103-get就是线程http-nio-8080-exec-1的子线程。

那么我们如果想在子线程中获取父线程的ThreadLocal值,应该怎样去做呢?

只需要改变一个地方,那就是将ThreadLocal替换成InheritableThreadLocal:

重启项目,测试:



子线程中可以获取到父线程中ThreadLocal的值了!

六、InheritableThreadLocal

InheritableThreadLocal是ThreadLocal的子类。

那么值究竟是从哪里获取出来的呢?

首先看一下InheritableThreadLocal类中相对于ThreadLocal的改动:

  • 重写了getMap方法:
    1
    2
    3
    ThreadLocalMap getMap(Thread t) {
    return t.inheritableThreadLocals;
    }
  • 重写了createMap方法:
1
2
3
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

再看一下Thread的创建过程:


最终init方法(省略版):

1
2
3
4
5
6
7
8
9
10
11
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
.................
Thread parent = currentThread();
.................
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
.................
}

此处的currentThread()线程就是上文中的http-nio-8080-exec-1(父)线程。

此时inheritThreadLocals为true,且parent线程的inheritableThreadLocals 不为空的,所以就会将parent的inheritableThreadLocals赋给新创建的线程的inheritableThreadLocals ,也就是上文中的Thread-103-get(子)线程。所以子线程中就有了父线程中所有ThreadLocal的值。

查看评论