ThreadLocal源码分析

ThreadLocal的使用

从ThreadLocal的名字可以知道,这是一个所属线程的局部变量。打个比方,ThreadLocal类就是一个存放数据的容器,容器里存储的是每个线程的私有数据。也就是说,只有当前线程可以访问,当然就是线程安全的了。按照惯例,先来一个ThreadLocal的”Hello World”示例:

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
import java.util.Date;
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadA a = new ThreadA("A");
ThreadA b = new ThreadA("B");
try {
a.start();
Thread.sleep(1000);
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Tools{
public static ThreadLocal<Date> t1 = new ThreadLocal<Date>();
}
class ThreadA extends Thread {
ThreadA(String name){
super(name);
}
@Override
public void run() {
try {
for (int i = 0; i < 2; i++) {
if (Tools.t1.get() == null) {
Tools.t1.set(new Date());
}
System.out.println(Thread.currentThread().getName()+" : "+Tools.t1.get().getTime());
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

输出:
A : 1515157910429
B : 1515157911428
A : 1515157910429
B : 1515157911428

ThreadLocal是在JDK包里面提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作的自己本地内存里面的变量,从而避免了线程安全问题,创建一个ThreadLocal变量后每个线程会拷贝一个变量到自己本地内存。
上述结果也验证了ThreadLocal类的线程隔离性,线程A和线程B的线程局部变量的值是不同的,虽然他们都使用了同一个ThreadLocal变量。但是,我们可以发现在第一次调用ThreadLocal类的get()方法返回值是null,能不能让ThreadLocal类具有默认的初始值呢?
答案是可以的,通过继承ThreadLocal类,重写其initialValue()方法即可,然后直接使用就可以了:

1
2
3
4
5
6
class ThreadLocalDefault extends ThreadLocal {
@Override
protected Object initialValue() {
return "默认的初始值";
}
}

总结:ThreadLocal类提供了另一种思路来解决共享资源的线程安全问题,除了控制资源的访问(锁)外,还可以通过增加资源来保证资源的线程安全,这也正是空间复杂度换取时间复杂度的体现。比如,让100个人一起吃饭,如果只有一双筷子,那么大家就要挨个吃饭,我们必须保证大家不会去哄抢这唯一的筷子,否则,谁也吃不了,这就是多线程的锁解决共享资源安全访问的方式。但是ThreadLocal提供了另一种思路,干脆发100双筷子,每个人一双,那么所有人都可以各自为营,大家就可以互不影响的吃饭了。

ThreadLocal实现原理

ThreadLocal是如何保证ThreadLocal容器里的对象只被当前线程所访问的?想知道答案,就得从ThreadLocal源码里找了。
二话不说,先上源码,我们先从set(),get()方法入手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
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()方法中,首先获得当前的执行线程对象,得到当前线程对象后,通过getMap()方法并将当前线程对象当作入参传入得到ThreadLocalMap对象。看到这里,一脸懵逼,这里怎么又来了一个ThreadLocalMap呢?
既然不知道是啥,那就点进源码看看。ThreadLocalMap是ThreadLocal的一个静态内部类,jdk源码里面的注释是这样解释的:“ThreadLocalMap is a customized hash map suitable only for maintaining thread local values.” 姑且可以将ThreadLocalMap理解为一个Map,同时Thread类中有一个内部成员变量的类型也是ThreadLocalMap,定义如下(从Thread类截取出来的):

1
2
3
4
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

总结一下:ThreadLocalMap就是一个定义在ThreadLocal类中的静态内部类,功能类似HashMap,同时在Thread类中有ThreadLocalMap类型的内部成员。

然后在看看getMap()方法:

1
2
3
4
5
6
7
8
9
10
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

很简单,直接返回Thread对象t的threadLocals成员变量,该变量的类型就是前面介绍的ThreadLocal.ThreadLocalMap。
后续的操作就很直接了,得到了当前线程对象的ThreadLocalMap成员后,如果不是null,那么就将当前的ThreadLocal对象作为key,set()方法的入参数也就是线程局部变量T作为value,存入ThreadLocalMap。如果是第一次访问ThreadLocal.ThreadLocalMap,就创建一个Map,然后存入键值对。
再总结一下:线程局部变量的值是保存在Thread类本身的,通过线程局部变量ThreadLocal作为key维护在Thread类的成员变量ThreadLocalMap(简单理解为一个Map)中。这样一来,线程的ThreadLocalMap就可以为线程关联多个线程局部变量ThreadLocal了,并且保证只有当前线程可以访问。

讨论完set()方法,get()方法自然就是将这个Map(ThreadLocalMap)中的数据拿出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}

首先也是获取当前线程对象的ThreadLocalMap对象,然后通过将自己this(ThreadLocal)作为key取得内部的线程局部变量的值。

内存泄漏

在了解ThreadLocal的内部实现后,我们知道线程局部变量的值其实是保存在Thread类的内部的,这也意味着只要线程不退出,那么线程局部变量的值就会一直存在,所以建议的使用方法是当不需要使用这个线程局部变量的时候可以通过调用ThreadLocal变量的remove方法,从当前线程的ThreadLocalMap里面删除该本地变量。
我们在使用线程池的时候,线程池内的线程未必会退出。这样一来,将一些大对象设置到线程的ThreadLocalMap中,可能会导致系统出现内存泄漏。因为如果不手动remove无用的线程局部变量,只要线程不终止,这些线程局部变量即使没有用处了还是无法被回收。
jdk源码也为此做出了优化,我们在此之前都是将ThreadLocalMap视为一个Map,现在更精确地说,它更加类似一个WeakHashMap。从源码来看,ThreadLocalMap的实现使用了弱引用,在JVM垃圾回收的时候,弱引用会比正常引用更快地被回收。以下是ThreadLocalMap中Entry的定义,可以看到为了更加高效的被回收,Entry继承了弱引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

总结:ThreadLocal为我们提供解决多线程安全访问共享资源的解决方案,但是如果使用不当也会给我们带来致命的灾难,所以在线程中使用完ThreadLocal变量后,建议及时remove掉。

0%