ThreadLocal详解

ThreadLocal详解

ThreadLocal是一个本地线程副本变量工具类,主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰。

说人话就是,ThreadLocal在每个线程都创建副本,每个线程可以访问自己的副本,线程之间相互不影响。

主要方法

void set(T value)

public void set(T value) {
  //获取当前线程
  Thread t = Thread.currentThread();
  //获取ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null)
    //不为空设置
    map.set(this, value);
  else
    //为空创建ThreadLocalMap
    createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
  //返回线程Thread对象中维护的ThreadLocalMap变量
  return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
  //创建一个新的ThreadLocalMap,并绑定到t中
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

T get()

public T get() {
  //获取当前线程
  Thread t = Thread.currentThread();
  //获取当前线程维护的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    //获取ThreadLocalMap.Entry对象
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      //不为空,直接返回
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  //为空,返回默认值
  return setInitialValue();
}

private Entry getEntry(ThreadLocal<?> key) {
  //获取当前key对应的索引
  //这个方法比较有趣,详细请看[threadlocal中hash](https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/81124944)
  int i = key.threadLocalHashCode & (table.length - 1);
  //获取Entry对象,实际存储信息的对象
  Entry e = table[i];
  if (e != null && e.get() == key)
    //hash不冲突,并且不为空,返回对象
    return e;
  else
    //hash冲突,或者e对象为空的情况下
    return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  Entry[] tab = table;
  int len = tab.length;

  //不空,即代表hash冲突
  while (e != null) {

    ThreadLocal<?> k = e.get();
    if (k == key)
      return e;
    //如果key为空,清除,避免内存泄漏
    if (k == null)
      expungeStaleEntry(i);
    else
      //向后查找一个1,即i = i+1
      i = nextIndex(i, len);
    e = tab[i];
  }
  //e为空的情况下返回空
  return null;
}

private T setInitialValue() {
  //初始化null
  T value = initialValue();
  //获取当前线程
  Thread t = Thread.currentThread();
  //获取当前线程维护的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null)
    //设置对象
    map.set(this, value);
  else
    //新建一个map
    createMap(t, value);
  return value;
}

//初始化一个空对象
protected T initialValue() {
  return null;
}

void remove()

public void remove() {
  //获取当前线程的ThreadLocalMap
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null)
    //不为空的话,直接清除当前对象的Entry
    m.remove(this);
}

private void remove(ThreadLocal<?> key) {
  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,有的话清楚
    if (e.get() == key) {
      e.clear();
      expungeStaleEntry(i);
      return;
    }
  }
}

内存泄漏

什么叫内存泄漏?

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

广义并通俗的说,就是:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

ThreadLocal的代码结构,如图所示:

threadLocal

由此,我们可以知道,一般的调用链为:

ThreadPool -> Thread -> ThreaLocalMap -> Entry -> value

如果此时,ThreadLocal使用完之后,讲栈中它的变量置为null,ThreadLocal 对象下一次 GC 会被回收,那么 Entry 中的与之关联的弱引用 key 就会变成 null。如果此时当前线程还在运行(线程池情况),那么 Entry 中的 key 为 null 的 Value 对象并不会被回收(存在强引用);如果当前线程执行完毕会被回收(ThreaLocalMap的生命周期和Thread生命周期一起完结),那么 Value 自然也会被回收。

为什么Value存在强引用?

可以通过数据结构看出来,v变量被指向value变量,这个就是强引用

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

以上,就是ThreadLocal内存泄漏的来源。那么,如何避免?

ThreadLocal 为了降低内存泄露的可能性,在set、get、remove操作时,会清除线程ThreadLocalMap中Entry所有数组中key为null的value值。

所以,当前线程使用完 threadlocal 后,我们可以通过调用 ThreadLocal 的 remove 方法进行清除从而降低内存泄漏的风险。