深入浅出ThreadLocal的实现原理

ThreadLocal的简介

ThreadLocal称为线程局部变量。在每个线程中都有自己独立的ThreadLocal变量。
每个线程中可有多个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
public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<Integer>();
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "默认值";
}
};
public static void main(String[] args) {
int temp = 100;
new Thread(() ->{
threadLocal1.set(temp + 1);
threadLocal2.set("线程A");
System.out.println("线程:" +Thread.currentThread().getName() + "中 threadLocal1 的值为: " + threadLocal1.get());
System.out.println("线程:" +Thread.currentThread().getName() + "中 threadLocal2 的值为: " + threadLocal2.get());
}).start();
new Thread(() ->{
threadLocal1.set(temp - 1);
System.out.println("线程:" +Thread.currentThread().getName() + "中 threadLocal1 的值为: " + threadLocal1.get());
System.out.println("线程:" +Thread.currentThread().getName() + "中 threadLocal2 的值为: " + threadLocal2.get());
}).start();
}
}

输出:

1
2
3
4
线程:Thread-0中 threadLocal1 的值为: 101
线程:Thread-0中 threadLocal2 的值为: 线程A
线程:Thread-1中 threadLocal1 的值为: 99
线程:Thread-1中 threadLocal2 的值为: 默认值

可以看到每个线程都保存着自己独立的变量threadLocal1、threadLocal2,互不影响。

ThreadLocal实例通常定义成 static 变量,因此每个线程都能访问到该实例。

抛出问题:为什么每个线程通过ThreadLocal实例时可以拿到自己设置的值?是如何实现的?

ThreadLocal 的实现原理

ThreadLocal类主要有四个方法set()、get()、remove()、setInitialValue(),要想了解它的实现原理,那么就来看看这几个主要方法是如何实现的。

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);
}
getMap(): 获取ThreadLocalMap
1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

threadLocals变量在线程Thread类中定义:

1
ThreadLocal.ThreadLocalMap threadLocals = null;

每个线程都有一个threadLocals变量即ThreadLocalMap对象,ThreadLocalMap类是ThreadLocal类的静态内部类,用来存储相应的值,总之就是一个Map,后面会详细讲到。

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

set()方法的过程如下:

1. 当前线程调用ThreadLocal.set()方法时,首先获取当前线程对象t;

2. 通过当前线程对象t获取到t线程中的threadLocals,即ThreadLocalMap

  • 如果ThreadLocalMap存在,那么更新value,key为当前ThreadLocal对象;
  • 如果ThreadLocalMap不存在,那么就根据当前线程对象t创建一个ThreadLocalMap,并将value存入。

到这里,基本就能回答开头所提出的的问题,大体上知道了ThreadLocal的实现原理。再来看get()方法的实现。

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);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

从上面可以看到,当调用get() 方法时,实际上就是先从当前线程中获取ThreadLocalMap,然后再根据当前this对象即ThreadLocal对象来获取对应的值。如果map不存在或者this对象的key不存在,那么就返回设置的初始值。

到这里就可以总结一下ThreadLocal的实现原理:每个线程都有一个自己的ThreadLocalMap对象,用来存储以ThreadLocal为key、Object为值的键值对,线程与线程之间互不影响。

原理图

image

remove() 方法

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

当ThreadLocal对象调用该方法时,获取到线程的ThreadLocalMap,移除以this对象(该ThreadLocal对象)为key的键值对。

setInitialValue() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
protected T initialValue() {
return null;
}

该方法主要作用是用来返回初始值,即initialValue()中的值,默认为null,在新建ThreadLocal时可以重写该方法,设置一个初始值。

ThreadLocalMap

前面提到,ThreadLocalMap就是存储相应变量的地方。
ThreadLocalMap和大多数容器一样,维护了一个内部数组,即Entry[]数组,Entey节点如下:

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

Entey是一个以ThreadLocal为key,Object为value的键值对,另外Entey继承了WeakReference(弱引用),在Entry的构造方法中,调用了super(k)方法将ThreadLocal实例包装成一个WeakReference。

为什么使用弱引用(ThreadLocal)作为key?

如果是强引用的话,ThreadLocalMap中一直会持有ThreadLocal的强引用,如果没有手动删除,那么ThreadLocal对象就无法回收,导致内存泄漏。

我们知道弱引用无论内存是否足够都会被GC回收。这样当没有强引用指向ThreadLocal对象时就可以被回收,因此也就不会出现ThreadLocal对象的内存泄漏。但还是会出现另一种内存泄漏问题,见下面问题。

为什么会引起内存泄漏?什么时候发生内存泄漏?如何防止内存泄漏?

在线程的生命周期内发生内存泄漏。

我们知道ThreadLocalMap中存储的是key为ThreadLocal的引用,当这个引用失效时即为null时,那么线程中就存在ThreadLocalMap的键值对,此时无法获得对应的Value,于是就存在一条Thread Ref -> Thread -> ThreaLocalMap -> Entry -> Value 强引用链,无法访问到Value,因此就出现了内存泄漏的问题。

防止内存泄漏:
1. 在ThreadLocalMap的set()、get()、remove()方法中,有一个将key为null的Entry擦除的过程,这样Entry内的value也就没有强引用链,自然会被回收。(不能保证一定擦除)
2. 当使用完毕后,显示调用remove()方法,直接清除ThreadLocalMap中以ThreadLocal对象为key的键值对;

扩展

ThreadLocalMap使用开放地址法来处理Hash冲突,而不是拉链法(HashMap、concurrentHashMap)。
主要原因是:在ThreadLocalMap中的散列值分散的十分均匀,很少会出现冲突。并且ThreadLocalMap经常要清除无用的对象,使用纯数组更加方便。

开放地址法:当发现有Hash冲突的时候,不会创建链表,而是继续在数组中寻找空的单元。探测数组中空单元的方式有很多,如线性探测法:从冲突的数组单元开始,依次往后搜索空单元,如果到了尾部还未找到就再从头开始查找,直到找到为止。

ThreadLocalMap键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map键值对存储线程变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。还有一个原因,如果是使用Map的方式存储线程变量,还要考虑到增加减少线程时的并发问题。

小结

  • ThreadLocal由于在每个线程中都创建了副本,因此threadLocal会占用一定的内存;是一种空间换时间的思想;
  • threadLocal只是一个工具,封装了ThreadLocalMap对象方法的入口;
  • threadLocal可以用来解决数据库连接、Session管理等问题,在spring也有大量使用,比如HttpServletRequest也是基于ThreadLocal来实现的。
  • ThreadLocal适用于每个线程需要有自己单独的实例,并且该实例需要在多个方法中共享,但不希望被多线程共享的场景。