ThreadLocal(线程变量),是一个以当前ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据当前ThreadLocal对象查询到绑定在这个 线程上的一个值。

用法

创建

可以使用泛型指定存储的类型

private ThreadLocal<String> threadLocal = new ThreadLocal();

存值

threadLocal.set("ThreadLocal test");

取值

threadLocal.get();

基本实现

设置值

set方法:

public void set(T value) {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
    // 注意this为key
    map.set(this, value);
  else
    createMap(t, value);
}

/**
 * 每个Thread对象里面存储了一个ThreadLocalMap
 * Thread类中:
 *      ThreadLocal.ThreadLocalMap threadLocals = null;
 */
ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可见ThreadLocal在设置的时候先取到当前线程,到当前线程中获得ThreadLocalMap,也就是当前线程存储在ThreadLocal中的集合,并通过使用当前ThreadLocal对象为键进行设置或者对原有的值覆盖。

Thread类代码片段:

public class Thread implements Runnable {
  ....
  /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
  ThreadLocal.ThreadLocalMap threadLocals = null;
  ....
}

ThreadLocalMap是存储在Thread中的一个成员变量,他绑定了当前线程中所有存储在ThreadLocal中的值,他的key为ThreadLocal对象,所以,我们才可以在同一个线程中每new一个ThreadLocal对象都可以存储值,也可以通过ThreadLocal对象在当前线程中获得绑定的值。

获取值

get方法:

public T get() {
  Thread t = Thread.currentThread();
  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();
}

/**
 * 每个Thread对象里面存储了一个ThreadLocalMap
 * Thread类中:
 *      ThreadLocal.ThreadLocalMap threadLocals = null;
 */
ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}

// 设置初始值
private T setInitialValue() {
  T value = initialValue();// return null;
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
  return value;
}

可以看到,取值的时候先到当前线程中取到ThreadLocalMap,然后通过当前的ThreadLocal对象为键在该map中取值,如果取到的ThreadLocalMap为null,那么就去设置ThreadLocalMap为初始值,初始值是当前线程为键,值为null。

为什么要使用ThreadLocal对象为键而不是当前线程为键绑定对象呢?

可以看到,ThreadLocalMap是存放于Thread对象中的成员变量,本质上也是一个map,key使用的是ThreadLocal对象,如果要使用当前线程为key的话就不需要再使用一个ThreadLocal对象作为工具类了,可以直接在Thread对象中存储一个List,在这里引入一个ThreadLocal的话本质上也就是可以通过不同的场景创建不同的ThreadLocal对象进行存储值。

ThreadLocalMap

结构

ThreadLocalMap是将当前ThreadLocal对象与值绑定在一起的放在ThreadLocal中的内部类,本质上是一个Map,但是却没有实现map接口,自己内部实现了一套Map结构。

ThreadLocalMap内部还有一个内部类Entry,该Entry对象就是采用K-V的形式组织存储数据,他的key就是我们设置的ThreadLocal对象,值就是我们设置绑定在该ThreadLocal对象中的值。该Entry继承了WeakReference对象是一个弱引用(弱引用,生命周期只能存活到下次GC前)

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

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

该Entry存储在ThreadLocalMap中是以数组的方式存储的。

private Entry[] table;

ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1及线性探测,寻找下一个相邻的位置。

所以ThreadLocalMap解决hash冲突的效率很低,如有大量不同的ThreadLocal对象放入map中时会发送冲突。所以如果要在一个线程中存储多个值的时候建议使用一个ThreadLocal对象为键,一个指定的对象为值存储,所有要存储的都放到这个对象里面,最大幅度的减少hash冲突发生的情况。

图示:

ThreadLocal原理与用法_内存泄漏


弱引用

ThreadLocalMap对象中存储值的Entry中对其key采用了弱引用的方式。

官网是这么解释的:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. 为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。

首先我们需要知道一个点就是ThreadLocal的生命周期是与线程的生命周期一致的,如果使用强引用的话,当ThreadLocal不在需要而进行回收的时候,如果没有进行手动的删除,那么在Thread对象中的ThreadLocalMap中会存在该不在使用的ThreadLocal对象的引用,导致无法回收造成内存泄露。

如果使用了弱引用,当ThreadLocal不在使用的时候即使不进行手动删除,那么他也只能存活到下一次进行垃圾回收的时候。

问:即使ThreadLocal本身由于弱引用机制已经回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry。导致内存泄漏?

ThreadLocal内部已经为我们做了一定的防止内存泄漏的工作。ThreadLocalMap在下一次调用setgetremove的时候会清除掉所有的的线程ThreadLocalMap中Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

弱引用导致的疑问?

ThreadLocalMap中key是弱引用,如果被GC了,那么线程再去get数据岂不是就没有了?

答:虽然自身是弱引用,但是使用的时候会存在声明ThreadLocal是普通引用,只要能get()那么就一定还存在普通引用,那么就不会因为是弱应用回收。

ThreadLocalMap调用setgetremove时候会调用方法进行清除Entry中Key为null的Value。

private int expungeStaleEntry(int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;

  // expunge entry at staleSlot
  tab[staleSlot].value = null;
  tab[staleSlot] = null;
  size--;

  // Rehash until we encounter null
  Entry e;
  int i;
  for (i = nextIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();
    if (k == null) {
      e.value = null;
      tab[i] = null;
      size--;
    } else {
      int h = k.threadLocalHashCode & (len - 1);
      if (h != i) {
        tab[i] = null;

        // Unlike Knuth 6.4 Algorithm R, we must scan until
        // null because multiple entries could have been stale.
        while (tab[h] != null)
          h = nextIndex(h, len);
        tab[h] = e;
      }
    }
  }
  return i;
}

总结

  • • ThreadLocal可以使用set方法向当前线程中存值,get方法进行取值
  • • ThreadLocal存储方式主要是以本身对象为key,要存储的值为value存储在ThreadLocalMap中并作为Thread中的成员变量
  • • 每个ThreadLocal只能保存一个变量副本,如果想要一个线程能够保存多个副本以上,就需要创建多个ThreadLocal
  • • 每一个线程中都有一个ThreadLocalMap
  • • ThreadLocalMap中的entry被设置为弱引用可以防止内存泄漏

Thread Local存在的问题

  • • 线程复用会产生脏数据,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被 重用。如果没有调用 remove 清理与线程相关的 ThreadLocal 信息,那么假如下一个线程没有调用 set 设置初始值就可能 get 到重用的线程信息。
  • • ThreadLocal 还存在内存泄漏的问题,由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此 当 ThreadLocal 被垃圾回收后,value 依旧不会被释放。因此需要及时调用 remove 方法进行清理操作。

工具类

import java.util.*;

public final class ThreadLocalUtil {
  private static final ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal() {
    protected Map<String, Object> initialValue() {
      return new HashMap(4);
    }
  };

  public static Map<String, Object> getThreadLocal(){
    return threadLocal.get();
  }
  public static <T> T get(String key) {
    Map map = (Map)threadLocal.get();
    return (T)map.get(key);
  }

  public static <T> T get(String key,T defaultValue) {
    Map map = (Map)threadLocal.get();
    return (T)map.get(key) == null ? defaultValue : (T)map.get(key);
  }

  public static void set(String key, Object value) {
    Map map = (Map)threadLocal.get();
    map.put(key, value);
  }

  public static void set(Map<String, Object> keyValueMap) {
    Map map = (Map)threadLocal.get();
    map.putAll(keyValueMap);
  }

  public static void remove() {
    threadLocal.remove();
  }

  public static <T> Map<String,T> fetchVarsByPrefix(String prefix) {
    Map<String,T> vars = new HashMap<>();
    if( prefix == null ){
      return vars;
    }
    Map map = (Map)threadLocal.get();
    Set<Map.Entry> set = map.entrySet();

    for( Map.Entry entry : set ){
      Object key = entry.getKey();
      if( key instanceof String ){
        if( ((String) key).startsWith(prefix) ){
          vars.put((String)key,(T)entry.getValue());
        }
      }
    }
    return vars;
  }

  public static <T> T remove(String key) {
    Map map = (Map)threadLocal.get();
    return (T)map.remove(key);
  }

  public static void clear(String prefix) {
    if( prefix == null ){
      return;
    }
    Map map = (Map)threadLocal.get();
    Set<Map.Entry> set = map.entrySet();
    List<String> removeKeys = new ArrayList<>();

    for( Map.Entry entry : set ){
      Object key = entry.getKey();
      if( key instanceof String ){
        if( ((String) key).startsWith(prefix) ){
          removeKeys.add((String)key);
        }
      }
    }
    for( String key : removeKeys ){
      map.remove(key);
    }
  }
}