最近看面经经常能看到面试官对ThreadLocal方面的提问,于是就去翻了翻ThreadLocal的源码,发现源码并不长,大概看了一通,能看出其中的七七八八,打算借此来梳理一下。PS:HashMap的源码能看明白,看这个源码也不是问题。

一、基本认识

点进源码,看见ThreadLocaljava.lang包下的类;在网上也看了看其他人对它的概括,加上自己对看完源码后对ThreadLocal的理解:

用于线程之间数据的隔离。简单说就是通过ThreadLocal来开辟一块区间存放数据,这个区间作为线程的本地线程存储,只有当前线程才能获取到这个数据,这个数据对其他线程是不可见的。

可以看到ThreadLocal的公有构造方法:

1
2
public ThreadLocal() {
}

就是用来创建ThreadLocal对象,没有在其中做其他的工作。

平常用ThreadLocal最多的两个方法就是setget两个方法;那我们来看一看这两个方法的源码。

二、set方法探析

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);
}

首先在使用set方法时,能够看到它先通过Thread的静态方法,来获取到当前线程。这个很关键,因为后面的操作,都是基于当前线程进行的操作,这也是为什么ThreadLocal能够做到线程之间数据隔离的。具体如何做到的,还要继续往下面看。

获取到当前线程后,通过getMap(t)方法来获取到一个ThreadLocalMap类型的map对象;来,点进getMap方法看看里面是啥:

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

返回的是当前这个线程的属性——threadLocals,继续点进:

1
ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到初始时,这个线程的该属性为null;现在先不管ThreadLocalMap具体是什么,只要明白该类可以由实例化的一个线程内部的属性指向ThreadLocalMap的实例即可。

下面的逻辑就比较简单了,如果map不为null,就能将value设置到map中,可以看见key是由当前的ThreadLocal对象作为的key;而值是由我们传递的value;如果为null,则进行createMap操作,从方法名也能看出,是先创建出这个map,然后后续再进行赋值的操作。

好了,可以看出,搞明白这个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
static class ThreadLocalMap {

private Entry[] table;

private static final int INITIAL_CAPACITY = 16;

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

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);
}
}

这里只列出了该类的部分方法和属性,如果谈到其他的方法和属性,到时再列出来。

首先该类是ThreadLocal中的静态类,诶,那为什么Thread也能访问到呢??别忘了,我们开头就提到ThreadLocaljava.lang包下的类,当然ThreadLocalMap同样是,而Thread也是,所以该类对Thread具有包可见性。

能够看到它其中一个属性为Entry继承了WeakReference,弱引用。也就是说正常情况下,Entry对象的生命周期只能存活到下一次垃圾收集为止。这里Entry是作为ThreadLocalMap中的table数组,也就是map用来散列的table

通过其构造方法,能够看出去。将table设置为容量为16,然后通过keyhash来计算散列到的位置i;然后创建Entry节点设置到table[i]处,最后设定table此时的容量为1并设定阈值;设定阈值的方法就不继续看了,就是为了后续方便扩容的判断。到这里大致能看明白ThreadLocalMap的数据结构大致长什么样了;

再回到上面createMap方法处:

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

创建ThreadLocalMap,并以当前线程引用这个Map;同时,当前的ThreadLocal对象作为key,我们传入的value作为Entry节点的value。此时结构如下图所示:

1

这个时候,我们再看一看当map已经存在时,调用map.set(this,value)时的操作:

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 set(ThreadLocal<?> key, Object value) {
Entry[] tab = table; // 首先引用当前ThreadLocalMap中的table
int len = tab.length; // 计算table的长度
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) { // 与当前探查的key相等直接替换 entry中的值 返回即可
e.value = value;
return;
}

if (k == null) { // 当前探查的key为Null 表明存在过期的entry,那么就开始在set的过程中清除这些过期的entry
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size; // hashtable中元素增加一个
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 从当前插入位置探查
rehash(); // rehash操作其中,包含删除旧条目及扩容操作
}

其实就是hash的插入方式,通过hash计算到索引的位置,也就是table中的位置,只是这里解决哈希冲突的方式是通过线性探查法进行冲突的解决(HashMap中是通过拉链法进行的解决)。

其中整个for循环的逻辑是,根据hash函数定位到关键字的位置后,如果当前table[i]上面存在了元素,那么表明可能发生了冲突或者就是同一个key,进入for循环中,一个一个判断是否插入的key和冲突的key相等,如果相等那就直接替换该entry上的value

如果在探查过程中发现table中的key为null时:这块逻辑比较复杂,目前读源码理解的是,将过时的entry进行删除,用没有过时的key-value进行覆盖,目的是为了防止在后续线程探查解决冲突的时候,冲突的key在数组中是离散的并不是紧凑的;

最后当探查到一个不存在entry时,表明查找到了应该插入的位置,那么就新建一个entry节点,并将该entry节点设置到该位置上。下面是清除过期entry的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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]; // 当前的entry节点
if (e != null && e.get() == null) { // 如果key不为Null,但是value为Null
n = len;
removed = true; // 标记 存在清除过程
i = expungeStaleEntry(i); // 移除这个过期的entry节点
}
} while ( (n >>>= 1) != 0); // 每次 除2 探查,采用启发式扫描探查过期的entry
return removed;
}

三、get方法探析

大致明白了set方法后,再看get方法会比较轻松。

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
Thread t = Thread.currentThread(); // 获取到当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 通过key获取到value
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

同样的获取到当前线程,并且从当前线程中获取到ThreadLocalMap;我们之前看到ThreadLocalMap中set值时,是将ThreadLocal作为key,所以我们通过将ThreadLocal传入获取到value;其中具体的逻辑就不看了,和set差不多,就是通过关键字定位进行比较,线性探查法那一套逻辑。

四、一些细节

4.1 ThreadLocalMap中的相关参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* The number of entries in the table.
*/
private int size = 0; // table中初始容量为0

/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0;扩容的阈值

/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) { // 装载因子为 2 / 3
threshold = len * 2 / 3;
}
4.2 ThreadLocal中的hash问题

我们知道这里的ThreadLocalMap是通过计算关键字keyhash进行散列的,而key就是不同的ThreadLocal对象,所以看看系统是如何分配这些散列值的。

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);
}

开始时ThreadLocal存在一个final静态变量HASH_INCREMENT;每次实例化ThreadLocal时,都带调用nextHashCode()方法,继而调用原子类的getAndAdd()方法,即:

1
2
3
public final int getAndAdd(int delta) { // delta -> HASH_INCREMENT -> 0x61c88647
return unsafe.getAndAddInt(this, valueOffset, delta);
}

所以看到是通过cas操作,给每个ThreadLocal对象加上固定的值——HASH_INCREMENT -> 0x61c88647;所以每个ThreadLocal对象的hash值是0x61c88647倍数;

4.3 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
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2; // 两倍扩容
Entry[] newTab = new Entry[newLen];
int count = 0;

for (int j = 0; j < oldLen; ++j) { // 进行节点的搬迁
Entry e = oldTab[j];
if (e != null) { // entry不为空进行搬迁
ThreadLocal<?> k = e.get();
if (k == null) { // 如果key 为null,表示过期
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1); // 重新根据hash计算散列的位置
while (newTab[h] != null) // 发生冲突,线性探查位置
h = nextIndex(h, newLen);
newTab[h] = e;
count++; // 计数
}
}
}

setThreshold(newLen); // 设置新的阈值
size = count;
table = newTab; // 新table代替旧table
}

通过注释也能看出,每次进行两倍的扩容。

五、Thread/ThreadLocal/ThreadLocalMap三者之间的关系

通过一张图进行说明:

2

上面表明两个线程,Thread-1,Thread-2;创建了三个ThreadLocal对象;每个Thread都有自己的ThreadLocalMap;而一个ThreadLocal对于一个Thread只能存放一个变量;所以每个线程的ThreadLocalMap中的key是不同的ThreadLocal;所以,看到ThreadLocal是每个线程可以共用的,但是由于相同的ThreadLocal是作为每个线程中独有的ThreadLocalMap中的key;所以,在不同的线程中,通过ThreadLocal获取到值不一样;是通过给线程设定独有的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
private static ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(16);

@Test
public void f1() throws InterruptedException {

ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
new Thread(() -> {
threadLocal1.set("张四");
threadLocal2.set("李五");
System.out.println(Thread.currentThread().getName()+":" + threadLocal1.get());
System.out.println(Thread.currentThread().getName()+":" + threadLocal2.get());
},"Thread-1").start();
Thread.sleep(10);
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+":" + threadLocal1.get());
System.out.println(Thread.currentThread().getName()+":" + threadLocal2.get());
System.out.println("==========");
threadLocal1.set("张四");
threadLocal2.set("李五");
System.out.println(Thread.currentThread().getName()+":" + threadLocal1.get());
System.out.println(Thread.currentThread().getName()+":" + threadLocal2.get());
},"Thread-2").start();
}
结果:
Thread-1:张三
Thread-1:李四
Thread-2:null
Thread-2:null
==========
Thread-2:张四
Thread-2:李五

看到在Thread-1中设置的值,在Thread-2中并获取不到。

七、ThreadLocalMap中的弱引用

可以看到ThreadLocalMap中的Entry节点继承了弱引用引用关系的ThreadLocal;也就是说ThreadLocalMap中的key是弱引用,当他不再关联强引用关系时,在下一次垃圾回收时,便会被回收;但是,其中的value是具有强引用关系的,因此会出现keynull,但是value不为null的情况,也就是说entry节点不为null,而强引用关系的value,它的生命周期同线程的生命周期,因此如果不手动设置为null,会造成内存泄露,继而会引发内存溢出。

在前面的setget等方法中,也能看到,他们在遍历的时候,如果发现keyNull的情况,便会进行清除工作,即将value设置为null

因此[1]:

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

正确使用方式:

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据;
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

八、使用示例

需求:在SpringBoot中自定义一个日志类,用于记录一个接口执行花费的时间。

思路:首先采用注解类,对于标志了注解的接口,统计该接口的一个请求所花费的时间;而对该接口的解析,通过设定过滤器,拦截每一个请求,检查请求的接口是否注解了该接口,如果采用了该注解那么就对该请求进行处理。

注解类:

1
2
3
4
5
6
7
8
9
10
11
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD) // 作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时
public @interface MyLog {

String desc() default ""; // 类的描述信息
}

Interceptor:

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
/**
* 日志拦截器
*/
public class MyLogIntercepter extends HandlerInterceptorAdapter {

// 采用ThreadLocal来隔离每个请求接口的线程
private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler; // 执行的方法
Method method = handlerMethod.getMethod(); // 获取方法对象
MyLog myLog = method.getAnnotation(MyLog.class); // 获取方法上的注解
if (myLog != null) { // 注解不为空
long startTime = System.currentTimeMillis(); // 开始时间
startTimeThreadLocal.set(startTime); // 设置执行的时间
}
return true;
}

/**
* 后置处理器
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {


HandlerMethod handlerMethod = (HandlerMethod) handler; // 方法拦截器
Method method = handlerMethod.getMethod(); // 获得被拦截的方法对象
MyLog myLog = method.getAnnotation(MyLog.class); // 获取方法上的注解
if (myLog != null) { // 存在该注解 则需要进行日志记录
long endTime = System.currentTimeMillis();
Long startTime = startTimeThreadLocal.get();
long optTime = endTime - startTime;
String requestURI = request.getRequestURI(); // URI
String methodName = method.getDeclaringClass().getName() + "." + method.getName(); // 全类名.方法名
String desc = myLog.desc(); // 方法描述
System.out.println("请求uri: " + requestURI);
System.out.println("请求方法名:" + methodName);
System.out.println("方法描述:" + desc);
System.out.println("方法执行时间:" + optTime + "ms");
}
}
}

添加拦截器到配置中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 这是一个自动配置类,容器启动时,将MyLogAutoConfiguration注入到容器中
*/
@Configuration
public class MyLogAutoConfiguration implements WebMvcConfigurer {

/**
* 想容器中注册新的拦截器
* 这个拦截器的作用:统计加入MyLog注解中的方法一个执行周期内的花费的时间
*
* @param registry: 被注册的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyLogIntercepter()).addPathPatterns("/**").excludePathPatterns("/images/**", "/js/**", "/css/**", "/toLogin", "/login", "/bootstrap-3.3.7-dist/**", "/favicon.ico", "/bizhi.jpg");
}
}

将自定义的过滤器进行注册,并设定拦截的路径;


Comment