引言

这篇文章,我们来看ThreadLocal的原理与使用。

一个例子

在讲解原理之前,我们先来看一个ThreadLocal的使用示例:

  1. public class ThreadLocalTest {
  2. private static final ThreadLocal<String> STRING_THREAD_LOCAL = new ThreadLocal<>();
  3. private static final ThreadLocal<Integer> INTEGER_THREAD_LOCAL = new ThreadLocal<>();
  4. private static void run(){
  5. Thread thread = Thread.currentThread();
  6. STRING_THREAD_LOCAL.set(thread.getName());
  7. INTEGER_THREAD_LOCAL.set(Integer.valueOf(thread.getName().replace("thread","")));
  8. System.out.println(thread.getName()+" string is " + STRING_THREAD_LOCAL.get());
  9. System.out.println(thread.getName()+" integer is " + INTEGER_THREAD_LOCAL.get());
  10. }
  11. public static void main(String[] args) {
  12. Thread thread1 = new Thread(ThreadLocalTest::run,"thread1");
  13. Thread thread2 = new Thread(ThreadLocalTest::run,"thread2");
  14. thread1.start();
  15. thread2.start();
  16. }
  17. }

这个示例中,有两个ThreadLocal类的变量,一个存储的是String类型,一个存储的是Integer类型,然后两个线程thread1和thread2分别在这两个ThreadLocal中存储了对应的值,最后两个线程分别将各自的String和Integer值输出:

  1. thread1 string is thread1
  2. thread2 string is thread2
  3. thread2 integer is 2
  4. thread1 integer is 1

可以看到ThreadLocal确实起到了线程本地变量的作用。

原理分析

从set方法看ThreadLocalMap

我们需要重点理解ThreadLocal是如何实现为每个线程保存不同的变量这一关键点。我们先看它的set方法:

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null)
  5. map.set(this, value);
  6. else
  7. createMap(t, value);
  8. }

首先,它拿到了当前线程,然后它调用了getMap方法,该方法的参数是当前线程,返回值是一个ThreadLocalMap对象,这个对象是ThreadLocal的内部类,也是理解ThreadLocal的重要一环。我们看getMap方法:

Thread与ThreadLocalMap

  1. ThreadLocalMap getMap(Thread t) {
  2. return t.threadLocals;
  3. }

逻辑很简单,它直接返回了当前线程的threadLocals字段。我们到Thread类中,能找到这个ThreadLocalMap类的变量的声明:

  1. /* ThreadLocal values pertaining to this thread. This map is maintained
  2. * by the ThreadLocal class. */
  3. ThreadLocal.ThreadLocalMap threadLocals = null;

也就是说,每个线程对象是有一个对应的ThreadLocalMap的。那这个ThreadLocalMap对线程来说,有什么意义呢?它跟ThreadLocal的关系是什么?我们回到set方法中继续看:
在else的逻辑中,如果当前线程的ThreadLocalMap不存在,它就会调用createMap方法:

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

这个方法也很简单,它就是实例化了一个新的ThreadLocalMap对象,然后赋值给了当前线程的threadLocals变量。我们重点看ThreadLocalMap初始化时的参数,第一个是当前ThreadLocal对象,第二个是我们调用set方法的参数,也就是要设置的值。到这里我们就能大胆地猜测一下,ThreadLocal本身并没有存储每个线程的值,它是将每个线程的值存储在该线程的ThreadLocalMap中,除了存储值,还存储了自身(ThreadLocal)的引用。试想一下,两个线程thread1和thread2同时调用set方法,createMap会将这两个线程要设置的值分别保存在它俩的ThreadLocalMap中。

ThreadLocalMap与ThreadLocal

那么ThreadLocalMap是怎样将ThreadLocal和值对应起来的呢?我们来看它的构造方法:

  1. /**
  2. * Construct a new map initially containing (firstKey, firstValue).
  3. * ThreadLocalMaps are constructed lazily, so we only create
  4. * one when we have at least one entry to put in it.
  5. */
  6. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  7. table = new Entry[INITIAL_CAPACITY];
  8. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  9. table[i] = new Entry(firstKey, firstValue);
  10. size = 1;
  11. setThreshold(INITIAL_CAPACITY);
  12. }

这里面用到了内部类Entry,先展示出来:

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. /** The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal<?> k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }

Entry实际上就是ThreadLocal和值的映射,它的构造方法中的第一个参数就是ThreadLocal,第二个参数是该ThreadLocal对应的值,也就是我们上面说的set方法的参数。
然后在ThreadLocalMap的构造方法中,首先创建了一个Entry的数组,为什么需要Entry的数组呢?因为一个线程对应一个ThreadLocalMap,但是一个ThreadLocalMap中需要保存多个ThreadLocal和对应的值,所以需要一个数组。有了数组之后,就需要知道当前的这个ThreadLocal放在Entry数组的第几个。这里用的是ThreadLocal的HashCode,每个ThreadLocal对象的threadLocalHashCode值不同并且是有规律的,这里不去详细介绍了。找到对应位置i之后,就会设置该位置上的Entry。之后还会有扩容等操作,这里也不详细介绍了。
经过上面的步骤之后,一个Thread对应的ThreadLocalMap就初始化成功了。

get方法

我们接着来看get方法:

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result;
  10. }
  11. }
  12. return setInitialValue();
  13. }

首先拿到当前线程,然后拿到当前线程的ThreadLocalMap,如果该ThreadLocalMap已经存在(被初始化过),就调用ThreadLocalMap的getEntry方法获取当前ThreadLocal对应的Entry:

  1. private Entry getEntry(ThreadLocal<?> key) {
  2. int i = key.threadLocalHashCode & (table.length - 1);
  3. Entry e = table[i];
  4. if (e != null && e.get() == key)
  5. return e;
  6. else
  7. return getEntryAfterMiss(key, i, e);
  8. }

确定Entry的方式还是通过ThreadLocal的threadLocalHashCode值。
然后Entry的value就是我们要查找的值。

删除线程本地变量

某个线程要想删除它在某个ThreadLocal中对应的值,可以通过调用remove方法来实现:

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m != null)
  4. m.remove(this);
  5. }

它首先拿到当前线程的ThreadLocalMap,然后执行ThreadLocalMap的remove方法来删除对应的ThreadLocal:

  1. private void remove(ThreadLocal<?> key) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. int i = key.threadLocalHashCode & (len-1);
  5. for (Entry e = tab[i];
  6. e != null;
  7. e = tab[i = nextIndex(i, len)]) {
  8. if (e.get() == key) {
  9. e.clear();
  10. expungeStaleEntry(i);
  11. return;
  12. }
  13. }
  14. }

小结

ThreadLocal实现了线程本地变量,通过将变量封闭在线程调用栈内,它也能避免很多并发的数据安全问题。ThreadLocal的实现依赖于ThreadLocalMap,而ThreadLocalMap与Thread是一一对应关系。当然,ThreadLocalMap里面很多逻辑还涉及到扩容和rehash。这里没有重点讲述。