浅析ThreadLocal

对于刚接触多线程的初学者来说,会发现很多框架都出现过ThreadLocal的身影,知道这个类是为了避免在多线程条件下出现资源竞争的问题。但是,在没有深入了解线程之前,更多的是知道ThreadLocal的目的是什么,对于是什么、怎么用、运行原理相信都是一个很模糊的概念,我希望这篇博客能够揭开困扰大家已久的谜团,以及带给大家一些相关知识。

说到要分析东西的时候,我觉得有一个比较好的方法可以帮助大家:不管分析什么,框架也好、源码也好,首先要对这个东西有一个概况了解、知道它是做什么的。千万不要一上来就看的很细很细,这样容易把自己绕晕而且效率很低(血的教训,哭);如果是分析JDK源码,尤其是集合框架的时候,可以先弄懂它的底层数据结构是什么,把底层数据结构弄懂了,那么分析这个类只是水到渠成的事情。哈哈

实现原理

其实就像我上面所说的那样,只要对线程有了一个深入的了解认识以后,就会很容易的理解ThreadLocal。

我们先用通俗的话解释一下ThreadLocal是一个什么东西,一句话:通过ThreadLocal保存的对象将在每一个使用它的线程留存一个副本,从而避免资源竞争,以空间换时间。

我们很自然的会有这样的疑问:

  1. ThreadLocal是如何将对象保存在各个线程中的呢?
  2. ThreadLocal又是如何获取保存在线程本地的对象的呢?

其实,我们可以这样猜:Thread应该有一个Map,保存了ThreadLocal维护的对象。
事实上ThreadLcoal和Thread也是这么做的:

  1. Thread类持有了ThreadLcoal类中有一个内部类ThreadLocalMap,通过将对象保存在这个Map中来实现每个线程都持有一个对象副本;
  2. 以ThreadLocal为Key,进行Key/Value存取操作。

ThreadLocal.java

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
//我们可以清晰看到get方法的逻辑:1、通过当前线程获取ThreadLocalMap;2、通过this(ThreadLocal)获取线程本地对象
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();
}
//set方法的逻辑很简单,如果map存在则set进去,不存在则先创建一个
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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;
}
}
}

Thread类中则持有了这个map,
Thread.java

1
ThreadLocal.ThreadLocalMap threadLocals = null;

如何工作

我们最关心的还是如何使用ThreadLocal,与使用息息相关的就是set()和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
34
35
36
37
38
39
40
41
42
43
44
public T get() {
//获取当前线程,用于获取ThreadLocalMap
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;
}
}
//如果为map == null,则执行setInitialValue()方法,进行初始化
return setInitialValue();
}
//设置初始值,逻辑与get()方法类似
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方法,子类必须实现这个方法以生成副本
protected T initialValue() {
return null;
}
//set方法的逻辑很简单,如果map存在则set进去,不存在则先创建一个
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

代码中的注释应该比较详细,这里就不写了~
如果有不懂的,欢迎留言~

WeakReference

WeakReference是干啥的

细心的同学肯定发现了ThreadLocalMap的内部类Entry继承了WeakReference:

1
static class Entry extends WeakReference<ThreadLocal<?>>

免不了想问WeakReference是干啥的?
我们可以很容易联想到Java中的四种引用关系,而WeakReference对应的是弱引用关系;
我们来看WeakReference的doc中第一句话,

1
2
3
4
Weak reference objects, which do not prevent their referents from being
made finalizable, finalized, and then reclaimed.
弱引用不会阻止它引用的对象变成finalizable、finalized状态并最终被回收

那,Entry为什么要继承WeakReference呢?

Entry为什么要继承WeakReference

如果线程太多(如线程池中的线程),那么保存的副本将会很多,而且不能回收。
因为thread没有运行结束那么会一直存在不能被回收,而threadLocals与对象副本之间是强引用的关系,所以这些副本也就不会被回收。大多数thread都是在阻塞等待状态,真正运行的thread只是少数,所以这些阻塞等待状态的thread所保存的副本就白白浪费了宝贵的内存资源。为了解决这个问题,JDK让Entry类继承了WeakReference类,这样当JVM进行GC的时候可以回收Thread保存的副本。这就意味着,副本对象必须是易于创建的,状态简单的对象,如数据库连接Connection。

Java中的四种引用

  1. 强引用
    只要强引用的存在,垃圾回收器永远不会回收这个对象;
  2. 软引用(SoftReference)
    当内存不足时,垃圾回收器将会回收被软引用引用的对象;
  3. 弱引用(WeakReference)
    垃圾回收器将会在下一次GC时回收被弱引用引用的对象
  4. 虚引用(PhantomReference)
    虚引用的存在与否完全不会影响垃圾回收器回收这个对象,虚引用存在的意义是对象被回收时发出一个通知。

扩展阅读

参考资料