ThreadLocal是什么?
ThreadLocal是一个关于创建线程局部变量的类。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。
ThreadLocal使用示例
示例1:ThreadLocal声明基本类型变量
示例1 代码段(1)
示例1 代码段(2)
执行程序,可以得到:
示例1 执行结果
从运行结果可以看出,对于基本类型变量,ThreadLocal确实是可以达到线程隔离作用的。
示例2:ThreadLocal声明自定义类型的对象
示例2 自定义类型
示例2 代码段(1)
示例2 代码段(2)
执行程序,可以得到:
示例2 运行结果
从运行结果可以看出,对于自定义类型的对象,ThreadLocal也是可以达到线程隔离作用的。
示例3:ThreadLocal声明的变量都指向同一个对象
示例3 对程序代码稍作修改
对示例2的代码稍作修改,使得ThreadLocal声明的变量初始化时不再实例化一个新的对象,而是让它指向同一个对象,运行查看结果:
示例3 运行结果
很显然,在这里,并没有通过ThreadLocal达到线程隔离的机制,可是ThreadLocal不是保证线程安全的么?这是什么鬼? 显然,虽说ThreadLocal让访问某个变量的线程都拥有自己的局部变量,但是如果这个局部变量都指向同一个对象的话,这个时候,ThreadLocal就失效了。
ThreadLocal源码剖析
ThreadLocal类的源码在java.lang包中。其中主要有四个方法:
1. get()
// 返回当前线程所对应的线程变量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; }
2. set(T value)
// 设置当前线程的线程局部变量的值。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); }
3. initialValue()
// 返回该线程局部变量的初始值。protected T initialValue() { return null; }
该方法定义为protected级别且返回为null,很明显是要子类重写来实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。
4. remove()
// 将当前线程局部变量的值删除public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
该方法的目的是减少内存占用,避免出现因为线程迟迟未结束而导致内存泄漏的情况。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
ThreadLocalMap类
从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类中几个核心的方法:
1. set(ThreadLocal<?> key, Object value)
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就可以看出他们的定义。
2. getEntry(ThreadLocal<?> key)
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回收,能够有效地避免内存泄漏。
ThreadLocal与内存泄漏
(注:本节参考了博文 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。
作者:chenjieping1995
链接:https://www.jianshu.com/p/53579bd5e12f