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 < ThreadLocal < ?>>{
- /** 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/ )
前面提到过,每个 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。
来源: http://www.jianshu.com/p/53579bd5e12f