ThreadLocal终于来了,虽然一直不知道它是个什么玩意
概述
ThreadLocal 是什么,ThreadLocal 是一个关于创建线程局部变量的类,简单来说是每个线程可以通过 get() 和 set() 来操作这个局部变量,而且这个局部变量是线程私有的,和其他线程无关系,没有冲突,其他线程也无法访问和修改。
简单举个例子就是:
public class ThreadLocalDemo {static ThreadLocal<Person> personThreadLocal = new ThreadLocal<>();public static void main(String[] args) {new Thread(() -> {try{Thread.sleep(2000);}catch (InterruptedException e){e.printStackTrace();}System.out.println(personThreadLocal.get());}).start();new Thread(() -> {try{Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}personThreadLocal.set(new Person());System.out.println(personThreadLocal.get());}).start();}static class Person{}}
在上面这个简单的代码中,下面的线程休眠了1s先创建了一个 Person 放入 ThreadLocal 对象,然后输出,上面的线程休眠了2s后手输出同一个 ThreadLocal 对象的内容。结果是下面的线程输出了正确的对象,和上面的线程输出null,原因正是因为下面的线程 set 的值是属于它自己的,上面的线程访问的仍是自己的还没 set 的null值,数据分离。
ThreadLocal
为什么同一个对象的 set() 方法和 get() 方法效果不一样呢?
直接康康 set() 方法:
public class ThreadLocal<T> {public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}static class ThreadLocalMap {//......}}
这个 set() 方法很简单,先获取一个 ThreadLocalMap,然后把当前的 ThreadLocal 对象作为 key 值,要保存的 value 作为 value存到这个 Map 里去,如果Map为空就创建一个。而这个 ThreadLocalMap 是 ThreadLocal 里的一个静态内部类。
然后进到 getMap() 方法里去,发现它获取的是 Thread 的 ThreadLocalMap,说明 ThreadLocalMap 其实是在 Thread 里的,好小子。
class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;}
到这里我们可以公开的情报是:我们在一个线程里向一个 ThreadLocal 对象 set 值的时候,其实是往当前线程里的 ThreadLocalMap 里将该 ThreadLocal 对象作为 key 值,存入我们需要的值 value。因此当你在另外一个线程了 get() 的时候,它其实是以这个 ThreadLocal 对象作为 key 值,去找自己的 ThreadLocalMap 里康康有没有对应的 value 值,因此就会出现上面说到两个线程 get() 到的值不一样的情况了,因为它们根本上就是存放在各自线程的 ThreadLocalMap 里作为局部变量,而不是我们认为的是同一个 Map,其他线程自然访问不到。
接下来我们康康这个 ThreadLocalMap
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 Entry[] table;//默认初始大小private static final int INITIAL_CAPACITY = 16;//阈值private int threshold; // Default to 0private void set(ThreadLocal<?> key, Object value) {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)]) {//获取引用,也就是key值ThreadLocal<?> k = e.get();//如果key和要存入的key一样,直接覆盖valueif (k == key) {e.value = value;return;}//如果为空,替换掉旧的entryif (k == null) {replaceStaleEntry(key, value, i);return;}}//创建了一个Entry,存放key/valuetab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}}
ThreadLocalMap 这个类内部维护了一个 Entry[] 数组来保存Entry,内部又定义了一个 Entry 类。而在这个 set() 方法里,最终是把一个 Entry 保存进这个数组里,而我们前面提到的 key 和 value 则保存在这个 Entry 里。
于是我们回头看这个 Entry 内部类,它继承自 WeakReference
从这个 Entry 我们可以看出,ThreadLocal 是使用了弱引用的。Java的四种引用不在这里展开讲述,弱引用的特点就是它只要遇到 GC 就会回收。

于是就上面的例子,我们可以得到这样一个结构:在主线程里,或者说 main 方法里,有一个对 ThreadLocal 的强引用,然后我们通过 ThreadLocal 向各自线程了 set 了一个 value 对象,它的 key 值如上所说的是个弱引用,引用到外面定义的一个 ThreadLocal 对象,而外面的 main 方法里定义的时候又有一个强引用引用了这个 ThreadLocal 对象。
此时如果我把这个 ThreadLocal 的强引用给置为 null,会发生什么呢?
如果说 ThreadLocal 的强引用给置为null了,那么对该 threadLocal 的对象只剩下Map里面的弱引用,按我们之前说的,弱引用遇到GC就会被回收,所以此时Map里该结点的key值是被回收了的。但是问题来了,value 值是一个强引用,它是不会被回收,但是key值又被回收了,此时这个value值永远不会被获取到,又不能被回收,就会造成内存泄漏问题。
这个内存泄漏问题其实算个伪命题,当该线程的生命周期结束了,即该线程结束了,因为 ThreadLocalMap 是依赖于线程的,所以包括value值在内的对象都会被回收掉。但是如果该线程的生命周期很长,这个内存泄漏的问题就会一直存在,时间长了就会造成内存溢出的结果。
因此为了解决这个内存泄漏的问题,ThreadLocal内做了一些措施来保证尽量不会造成内存泄漏:即在 get() 、set() 、remove() 方法中都会对该 ThreadLocalMap 里key值为null的Entry设置为null,帮助内存下次gc回收掉无用对象。
private void set(ThreadLocal<?> key, Object value) {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;}//这一步把key值为null的Entry值null,帮助gcif (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}
以下来自晟晟的笔记:
但是这样也并不能保证ThreadLocal不会发生内存泄漏,例如:
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
- 分配使用了ThreadLocal又不再二次调用get()、set()、remove()方法,那么就会导致内存泄漏。
另外在使用 ThreadLocal 的时候,我们也是建议至少调用一次 remove() 方法,显式地对内存进行回收,加快GC。
既然如此,为什么还要使用弱引用呢?
我们前面说过,这个 ThreadLocal 的内存泄漏其实是个伪命题,因为 ThreadLocalMap 的生命周期是跟随 Thread 的,当线程结束了,其依赖的内容都会被回收,其实不会造成内存泄漏。而可能造成内存泄漏的原因是当线程的生命周期特别长的情况下,而这种情况也因为对内部方法的实现得到了初步解决(除非你set了一个值之后就没后续了,那也就漏一个对象)。
反过来想想,如果使用的是强引用,但是 ThreadLocalMap 存的是 ThreadLocal 的强引用,无论外部 ThreadLocal 是否以及被弃用了,该 ThreadLocalMap 的这个 Entry 还是得不到回收,此时才是真正的内存泄漏。因此,弱引用往往是一个解决内存泄漏的更优解。
官方大大的解释: To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. 为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。
