ThreadLocal 存储线程本地变量,也就是说,每个线程都持有自己的一个副本,独立进行初始化和修改操作。

# 原理

# set & get

先来看下 set 和 get 操作。

public void set(T value) {
  // 当前线程
  Thread t = Thread.currentThread();
  // 从 map 中获取当前线程存储的所有 ThreadLocal 变量
  ThreadLocalMap map = getMap(t);
  //map 不为空的话直接设置当前 ThreadLocal 变量的值
  if (map != null)
    map.set(this, value);
  //map 为空的话创建 map 并设置当前 ThreadLocal 变量的值
  else
    createMap(t, value);
}
public T get() {
  // 获取当前线程
  Thread t = Thread.currentThread();
  // 获取当前线程的所有 ThreadLocal 变量
  ThreadLocalMap map = getMap(t);
  //map 不为空且当前变量的值不为空,获取当前 ThreadLocal 变量的值,否则创建 map 并将当前线程的变量值设为 null,最终返回 null
  if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
  // 当前线程所有的 ThreadLocal 变量存储在 Thread 中
  return t.threadLocals;
}

可以看出,每个线程都有一个 ThreadLocalMap,且这个 map 是 Thread 中第一个变量。

# ThreadLocalMap

# Entry

存储使用 Entry 数组,Entry 是弱引用,是为了防止线程还存活,但是线程中的本地变量已不再被外界使用。

static class Entry extends WeakReference<ThreadLocal<?>> {
	// 线程本地变量
  Object value;
  // ThreadLocal 变量作为 key,是弱引用,变量值作为 value
  Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
  }
}

# 变量

默认初始容量为 16,动态进行扩容,当容量使用到达数组长度的 2/3 时,进行扩容。

// 数组默认初始化容量
private static final int INITIAL_CAPACITY = 16;
// 存储所有本地变量
private Entry[] table;
// 所有本地变量的个数
private int size = 0;
// 扩容的阈值,一般设置为数组长度的 2/3
private int threshold;

# 构造函数

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  // 初始化数组
  table = new Entry[INITIAL_CAPACITY];
  // 元素放入数组下标位置为本地变量哈希值与操作数组下标最大值
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  // 将元素放入对应位置
  table[i] = new Entry(firstKey, firstValue);
  // 数组大小设置为 1
  size = 1;
  // 设置扩容阈值为当前大小的 2/3
  setThreshold(INITIAL_CAPACITY);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
  // 参数 map 的数组
  Entry[] parentTable = parentMap.table;
  // 初始化大小为指定 map 的数组长度
  int len = parentTable.length;
  setThreshold(len);
  // 初始化当前数组
  table = new Entry[len];
  // 遍历参数 map 的数组,赋值给当前数组,注意这里不是按照下标一一对应的赋值,也是要计算本地变量的本地哈希值与操作数组下标最大值,如果新的下标有元素,放入后面的位置
  for (int j = 0; j < len; j++) {
    Entry e = parentTable[j];
    if (e != null) {
      @SuppressWarnings("unchecked")
      ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
      if (key != null) {
        Object value = key.childValue(e.value);
        Entry c = new Entry(key, value);
        int h = key.threadLocalHashCode & (len - 1);
        while (table[h] != null)
          h = nextIndex(h, len);
        table[h] = c;
        size++;
      }
    }
  }
}

# 并发问题

可以看出,本地变量存储在每个线程上,所以每个线程只会访问自己的本地变量,不会访问到其他线程的,所以也不会有线程冲突问题。如果每个本地变量都是一个 map,那么 key 为线程标识,value 为变量值,这样的话会出现多个线程都要操作同一个 map 的场景。两者区别如图所示:

上图每个 map 需要被所有线程访问,下图每个 map 只需要被单个线程访问。

# 内存泄漏问题

内存泄漏常发生在线程复用的场景下,线程池中线程并不会被销毁,所以线程变量 threadLocals 也不会被销毁,但是 Entry 中的 key 是弱引用,当外部没有引用的情况下,ThreadLocal 会被销毁,也就是说,key 变成了 null。这种 key 为 null 的 Entry,在 set 或者 get 的时候,会进行清除,所以只要代码没有写搓,就不会发生内存泄漏。当然,最好在线程使用完毕前进行 clear。