聊一聊ThreadLocal

JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。 ——《百度百科》

聊一聊 ThreadLocal

一、什么是 ThreadLocal ?


ThreadLocal,线程局部变量,就是为每一个使用该变量的线程提供一个副本,同时每个线程都能独立地改变属于自己的副本而不会与其他线程的副本产生冲突。

在多线程共享资源上,我们常常需要控制线程对于资源的访问顺序、数目等来达到同步的效果。而 ThreadLocal 刚好与此相反,JVM 为每一个线程都绑定了本地存取的空间,各个线程访问属于自己的空间互不干扰。

二、ThreadLocal 定义


1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
/*** 省略部分参数、方法 ***/
// 仅显示两个主重要的方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}

从ThreadLocal 的概念和定义方法,我们可以知道其内部维护有且只有一个 Map 键值对,并且各个线程之间不能相互访问。好了,那么接下来就需要解决两个问题了,一个是线程如何维护自己独立的 Map ?另外,Map 的数据结构与普通的 Map 有什么区别(当然有区别了,不然也没必要定义内部类)?

如何维护 ThreadLocalMap?

平时的 Map 操作很简单,你现在需要解决的是然后避免自己的 Map 被其他线程访问到。我们继续跳进去源码中 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // getMap() 方法的源码在前面
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 获取当前 ThreadLocal 的变量值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。
}

追踪进去 Thread 类(JVM 为每一个线程都绑定了本地存取的空间) :

1
2
3
4
5
6
public
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}

这段源码很容易阅读,大意就是说,Thread 本身只有有一个 ThreadLocalMap,但线程局部变量执行 set 操作时,根据线程绑定只能对于本线程的局部变量操作。

ThreadLocalMap 的数据结构

继续跟踪进源码 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
private static final int INITIAL_CAPACITY = 16; // 初始化容量
private Entry[] table;
/*** 省略 ***/
}

其大意就是说,在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是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
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 计算 hash 值
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) { // 解决冲突
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

从这段源码,我们知道当执行插入操作时,先计算 ThreadLocal 对象的 hash 值从而定位到 table 数组的位置 i 。若当前位置为空,则初始化 Entry 对象并返回。若当前位置的 key 值相同,则执行更新操作。若当前位置已经有元素存在(发生冲突),那只能找下一个空位。

三、ThreadLocal 的同步机制

在正常的同步机制中,我们一般通过对象的锁机制来保证同一时间只有一个线程访问共享变量(用时间来换取空间),这样子程序设计以及编写难度较大。而 ThreadLocal 采用了独立变量副本来隔绝多线程对数据访问的冲突(用空间来换取时间)。

但是,ThreadLocal 不是为了解决多线程访问的问题,其只是为每个线程提供了一个独立的变量副本

一般情况下,ThreadLocal 主要应用将线程对应到实例的场景中,并且要求这个实例会很频繁应用到。举个栗子,比如聊天网站登录用户的信息:当用户登录成功后,如果用到了 WebSocket 之类的长连接,服务器端会保持线程的长久性,此时在线程中创建 ThreadLocal 用于存储用户基本信息,这样子在页面跳转时不会受到影响,同时也避免了对共享变量的频繁读写。类似于这种场景采用 ThreadLocal 来说是比较好的。

四、ThreadLocal 导致的内存泄露

此处资料来源于网络资源及书籍。

为什么 ThreadLocal 会导致内存泄露?

原因就是,在 ThreadLocal 中存储对象时,对于 key 值采用了 WeakReference 对象(PS: JVM中存在 强、弱、软、虚 四种引用),然后再插入到数组中。所以,当把 ThreadLocal 实例设置为 null 时,ThreadLocal 将会被 GC 回收。但是,value 却无法被回收,因为存在了一条 current Thread 的强引用,这就会导致内存泄露。一直持续到 Thread 结束,所有资源才能被回收。

可能这个时候有人认为没事……只要线程被销毁就没事了……那么我们假设在系统中使用 线程池技术 呢,线程结束后是不会被销毁的将会再次使用…………这样子导致内存泄露的问题将是致命的!

如何避免内存泄露?

既然找到了内存泄露的原因,找到其解决方案就不难了。只要在使用完 ThreadLocal 后及时调用 remove() 方法清除不要的 Entry 对象,就可以避免此个问题了。

评论