ThreadLocal是一个关于创建线程局部变量的类。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。
执行程序,可以得到:
从运行结果可以看出,对于基本类型变量,ThreadLocal确实是可以达到线程隔离作用的。
执行程序,可以得到:
从运行结果可以看出,对于自定义类型的对象,ThreadLocal也是可以达到线程隔离作用的。
对示例2的代码稍作修改,使得ThreadLocal声明的变量初始化时不再实例化一个新的对象,而是让它指向同一个对象,运行查看结果:
很显然,在这里,并没有通过ThreadLocal达到线程隔离的机制,可是ThreadLocal不是保证线程安全的么?这是什么鬼? 显然,虽说ThreadLocal让访问某个变量的线程都拥有自己的局部变量,但是如果这个局部变量都指向同一个对象的话,这个时候,ThreadLocal就失效了。
ThreadLocal类的源码在java.lang包中。其中主要有四个方法:
// 返回当前线程所对应的线程变量
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的成员变量 threadLocal
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,然后通过ThreadLocalMap获取当前ThreadLocal的键值对Entry,最后通过该Entry获取目标值result。
其中,getMap()方法可以获取当前线程所对应的ThreadLocalMap,其源代码如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 设置当前线程的线程局部变量的值。
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,如果不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,如果不存在,则调用createMap()方法新建一个,其源代码如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 返回该线程局部变量的初始值。
protected T initialValue() {
return null;
}
该方法定义为protected级别且返回为null,很明显是要子类重写来实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。
// 将当前线程局部变量的值删除
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
该方法的目的是减少内存占用,避免出现因为线程迟迟未结束而导致内存泄漏的情况。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
从ThreadLocal的源码中我们可以看到,ThreadLocal的实现比较简单,主要是依赖于ThreadLocalMap这个类,我们有必要好好理解一下后者。
根据命名就可以看出,ThreadLocalMap,它实际上是一个Map键值对。在其内部使用了Entry的方式来实现key-value的存储:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
在上面的代码中,Entry内的Key就是ThreadLocal,而Value就是线程私有的那个变量。同时,Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用是一个弱引用。
下面来看一下ThreadLocalMap类中几个核心的方法:
private void set(ThreadLocal> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
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();
}
源码的意思简单明了,根据要保存的key到Entry数组中去匹配,如果key已经存在就更新值,否则创建新的entry写入。
值得注意的是,这里的set()操作和我们在集合Map了解的put()方式有点儿不一样,虽然他们都是key-value结构,不同点在于他们解决散列冲突的方式不同。 集合Map的put()采用的是拉链法,即在每个数组元素的位置,存入链表来解决冲突。而ThreadLocalMap的set()则是采用开放定址法来解决冲突的。
set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。在set()方法中还有一个变量很重要:threadLocalHashCode,定义如下:
private final int threadLocalHashCode = nextHashCode();
从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量,从nextHashCode就可以看出他们的定义。
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);
}
由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们要找的元素,则返回,否则调用getEntryAfterMiss()再寻找,源码如下:
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。
(注:本节参考了博文 http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)
图:ThreadLocal实现原理
前面提到过,每个Thread都有一个ThreadLocal.ThreadLocalMap,该map的key为ThreadLocal实例的一个弱引用,我们知道弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收。
如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocal类的设计中已经考虑到这种情况,也加上了一些防护措施:在触发ThreadLocal的remove()时会清除线程ThreadLocalMap里key为null的value。