ThreadLocal概览

ThreadLocal简介

ThreadLocal是java中将非线程安全变为线程安全的一个神器.通过为每一个线程保存一个线程本地变量来保证数据的安全性,通过set()和get()方法来使用. 因为每个线程都拥有变量的副本.不和其他线程变量交互,所以不会出现线程安全问题.所以这里有一个点就是ThreadLocal应用的场景应该是变量的访问是没有依赖关系的,每个线程只和自己的数据副本打交道.

属性

1
2
3
4
5
6
7
8
9
10
public class ThreadLocal<T> {
//每个线程有自己的HashCode,并不是根据线程计算的,而是Local分配的
private final int threadLocalHashCode = nextHashCode();

//用来计算下一个线程的hash值,threadLocalHashCode就是根据这个Integer计算出来的,每次增加一个一个HASH_INCREMENT的值
private static AtomicInteger nextHashCode =
new AtomicInteger();
//hash增量,hash值的增量
private static final int HASH_INCREMENT = 0x61c88647;
}

主要方法

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

看代码可以知道首先获取当前线程,然后通过getMap获取到一个ThreadLocalMap.如果当前线程存在map的话,就从map中取出当前线程所需要的值.注意这里getEntry中传入的是this,不是当前线程t.因为map中存储的是键值对是ThreadLocal变量与ThreadLocal变量存储的值的对应.
接下来看getMap方法

getMap()

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

这里就是获得线程自己的ThreadLocalMap.每个线程都有一个ThreadLocal变量.在初始的时候是空的.

1
2
// 这是线程中的ThreadLocalMap变量
ThreadLocal.ThreadLocalMap threadLocals = null;

然后上面获取到的.如果获取到的map不是空的,说明已经设置过本地变量.因此就直接获取到此map的键值对.然后返回所要的值.
如果map为空,就通过setInitialValue()方法设置初始值.

setInitialValue()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

//这个方法是用来给子类继承的.在ThreadLoacl中SuppliedThreadLocal内部类继承了此类,用来实现这个方法.设置初始值
protected T initialValue() {
return null;
}

这个代码也很清晰明了了,如果当前线程有本地map,就直接添加默认值,如果没有,就使用createMap()创建一个

createMap()

创建Map其实就是给当前线程的ThreadLocal赋值.

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

接下来细看一下ThreadLocalMap类的内部实现

ThreadLocalMap

在这里map中的Entry存储的key是一个ThreadLocal变量,值就是设置的值.这里有一点要搞明白.每个线程都有一个ThreadLocalMap的变量.通过这个变量能够找到当前线程存储的每个的ThreadLocal所对应的值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static class ThreadLocalMap {
//这里在说一下,这里使用的是弱引用.是为了让在线程将此值释放掉后能够让虚拟机进行回收,当线程本地释放变量的时候,不需要在对map进行主动移除,虚拟机就会自动回收掉无用变量.
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//省略一些基本的map的操作与字段,里面也是使用数组来存储数据.使用线性探测来解决hash冲突.

private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

重点看一下getEntry,通过得到当前线程保存的ThreadLocal变量.然后在通过这个值找到对应表中的位置,在这里有一点可能会出现为空或者这个key值不是当前线程保存的本地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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 调用map的remove操作
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// for循环.向后遍历找到,如果碰见空,则直接返回.如果遇见自己,则执行删除操作.
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

// 上面看到删除节点时候会调yoga此方法.
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 先将数组中的两个值键值对设置为空,
tab[staleSlot].value = null;
tab[staleSlot] = null;
// 设置size--
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果此ThreadLocal为空,则删掉此项值
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

上面这个方法大概意思就是在删除某个节点的时候,会判断在他后面的节点有多少个可以向前移动,如果可以向前移动,就向前移动(那些经过二次探测或者三次探测的节点才有可能向前移动). 直到碰见一个空节点位置.

删除讲完了,对于线性探测法的缺点的改进也就很容易明白了,不会出现那种情况.因为每一次删除一个节点.都会将之后的连续的不为空的节点向前移(这里向前移动最多移动到自己节点的hash值与上len值的位置).也就是把删除掉的那些空位给补上.

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方法首先就获取当前线程,在获取一个ThreadLocalMap(),然后设置值

总结

到这里可能会有一些人对这个ThreadLocal还有点懵逼,通过一个完整的例子来看一下.

1
2
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();

比如说这里面有两个ThreadLocal,上面讲过,线程中存储的是一个ThreadLocalMap,map中存储的是ThreadLocal变量与值的映射.比如这里,线程中的threadLocalMap中会有两对键值对,一个是threadLocal的,另一个是threadLocal1的.当然,拥有这两个键值对的前提是调用了threadLocal.set()方法或者是get方法.如果调用了threadLocal.set(str)方法,那么就会在此线程的map中添加一个映射,就是threadLocal->str的映射.如果在获取的时候,通过获取线程的threadLocalMap,找到此threadLocal对应的值,就是此线程保存的这个threadLocal的值了.

ThreadLocal的实现原理主要就是通过每个线程保存一个ThreadLocalMap的变量,在ThreadLocal中保存的ThreadLoca => value 的键值对映射的一个map,通过找到线程对应的threadlocalmap,然后在从threadlocalmap中找到对应的threadlocal对应的值,就能够获取到对应的变量副本.