ThreadLocal主要用于线程之间的数据隔离,和单个线程不同方法之间的数据共享

ThreadLocalMap底层结构

ThreadLocalMap里有Entry数组,注意Entry的key是ThreadLocal,在set的时候,传进去的是this,也就是当前的ThreadLocal实例。

他的数据结构其实是很像HashMap,但是看源码可以发现,它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表。

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;
            }
        }
        ……
    }

image.png

//线程Thread持有的threadLocals变量是ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

在实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。

HashMap是使用拉链法解决hash冲突的,ThreadLocalMap是使用线性探测解决hash冲突的(内部只维护Entey数组,没有链表)。所以,源码中在清除泄漏的Entry时,会进行rehash,防止数组的当前位置为null后,有hash冲突的Entry访问不到的问题。我们先看一下源码

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;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)

然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上。

如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value。

如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止
image.png
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的

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);
}

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;
}

ThreadLocal对象存放位置

因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

具体的关系为:线程Thread持有ThreadLocalMap, ThreadLocalMap里面保存着Entry,Entry里面,key是当前的ThreadLocal对象,value是保存的值,这个ThreadLocal对象是一个线程不同的泛型声明的ThreadLocal。
就比如一个类里面声明了两个ThreadLocal,那么key就是这两个ThreadLocal对象。两个线程同时对一个ThreadLocal设置了值,那么这个值分别在这两个线程的ThreadLocalMap里面,虽然key都是这一个ThreadLocal,但是容器Map是不同线程的

如果我想共享线程的ThreadLocal数据怎么办

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值

内存泄露

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费.

内存泄漏是一时的,只要被执行rehash,或者重新赋值,则key为null的value则直接被回收

如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象一直会被TheadLocalMap的数组引用。就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。

线程没有结束,则他的hash所代表的索引下的Entry一直都在,但是Entry里的key是null,value则会泄漏。

内存泄漏解决

在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。或者只要让ThreadLocal具有线程的生命周期,就完全没必要使用remove方法,也完全不用担心内存泄漏的问题。

key为什么被设计为弱引用

弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生为止。 当垃圾收集器开始工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

在ThreadLocal的get、set的时候,都会检查当前key所指的对象是否为null,是则删除对应的value,让它能被GC回收。

tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

假如每个key都强引用指向ThreadLocal的对象,引用ThreadLocal的对象被回收之后,这个ThreadLocal对象就会因为和Entry对象存在强引用关联而无法被GC回收,key不为null也无法删除value,导致内存泄漏。除非线程结束后,线程被回收了,map也跟着回收。

但是如果使用了弱引用,引用ThreadLocal的对象被回收之后,这个ThreadLocal对象就会因为和Entry对象存在弱引用关联而在下一次GC的时候被回收,value在下一次ThreadLocalMap调用set,getremove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal本身不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。