刨根问底(一):ThreadLocal

一、什么是ThreadLocal

顾名思义,线程本地变量,用ThreadLocal修饰的变量在线程间相互独立,互不影响。

二、编码体验

创建测试程序,分别启用两个线程,在各个线程中设置并打印当前用户名,观察输出;

public class Test {

    private static ThreadLocal<String> usernameLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        usernameLocal.set("main");
        new Thread(() -> {
            System.out.println("[a]" + usernameLocal.get());
            usernameLocal.set("a");
            System.out.println("[a]" + usernameLocal.get());
        }).start();

        new Thread(() -> {
            System.out.println("[b]" + usernameLocal.get());
            usernameLocal.set("b");
            System.out.println("[b]" + usernameLocal.get());
        }).start();
        System.out.println("[main]" + usernameLocal.get());
    }
}

由于多线程的交错性,结果顺序不一,但最终结果相同,每个线程操作的用户名(usernameLocal)互不影响;

[main]main
[a]null
[b]null
[a]a
[b]b

三、源码剖析

那么ThreadLocal是怎么做到线程变量隔离的?先查看ThreadLocal源码一探究竟。

她的构造方法是一个空的方法,那么我们就从最基础的set开始ThreadLocal的刨根问底之旅;

1. set

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

Sets the current thread's copy of this thread-local variable to the specified value,将当前线程局部变量的副本设置为指定值。

值得注意的是ThreadLocalMap这个类,它是ThreadLocal的静态内部类,手动简化下;

2、ThreadLocalMap

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

  /**
   * The table, resized as necessary.
   * table.length MUST always be a power of two.
   */
  private Entry[] table;

  // 注:省略一些代码

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

  /**
   * Get the entry associated with key.  This method
   * itself handles only the fast path: a direct hit of existing
   * key. It otherwise relays to getEntryAfterMiss.  This is
   * designed to maximize performance for direct hits, in part
   * by making this method readily inlinable.
   *
   * @param  key the thread local object
   * @return the entry associated with key, or null if no such
   */
  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);
  }

  /**
   * Set the value associated with key.
   *
   * @param key the thread local object
   * @param value the value to be set
   */
  private void set(ThreadLocal<?> key, Object value) {

      // We don't use a fast path as with get() because it is at
      // least as common to use set() to create new entries as
      // it is to replace existing ones, in which case, a fast
      // path would fail more often than not.

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

  /**
   * Remove the entry for key.
   */
  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)]) {
          if (e.get() == key) {
              e.clear();
              expungeStaleEntry(i);
              return;
          }
      }
  }
}

与 map 结构非常类似,根据 get/set 方法可以判断出 key 是 ThreadLocal 类型的,然后 value 就是我们实际需要存放的值。

值得注意的 Entry 是继承自 WeakReference,构造方法中可以观察出弱引用只针对于 key,也就是 ThreadLocal 变量。

了解了ThreadLocalMap的结构,那么回过头继续阅读ThreadLocal的set方法,有个关键的getMap方法:

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

原来是获取的是线程ThreadLocalMap类型的私有变量threadLocals,这差不多就能解释上面的为什么问题了。看看Thread类下面的私有变量threadLocals:

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

初始值为null,然后我们在set中看到当map为null会走createMap方法:

/**
 * Create the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param t the current thread
 * @param firstValue value for the initial entry of the map
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

以当前的ThreadLocal对象实例为key,传入的值为value,初始化t线程的私有变量threadLocals(ThreadLocalMap),有兴趣可以回到上面看看这个构造方法。

当t线程的threadLocals已经初始化后,再进行set操作,就简单多了,直接类似map的put方法,和上面的初始化设置操作一样。

图示更为清晰:

3、get

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
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();
}

理解了set的操作,get就容易的多了。先根据当前线程获取到threadLocals,然后如果这个threadLocals不为空并且根据当前ThreadLocal为key取到的entry也不为空,才返回entry中保存的值,否则返回setInitialVal()方法的结果;

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

先看看第一行的initialValue这个方法:

/**
 * Returns the current thread's "initial value" for this
 * thread-local variable.  This method will be invoked the first
 * time a thread accesses the variable with the {@link #get}
 * method, unless the thread previously invoked the {@link #set}
 * method, in which case the {@code initialValue} method will not
 * be invoked for the thread.  Normally, this method is invoked at
 * most once per thread, but it may be invoked again in case of
 * subsequent invocations of {@link #remove} followed by {@link #get}.
 *
 * <p>This implementation simply returns {@code null}; if the
 * programmer desires thread-local variables to have an initial
 * value other than {@code null}, {@code ThreadLocal} must be
 * subclassed, and this method overridden.  Typically, an
 * anonymous inner class will be used.
 *
 * @return the initial value for this thread-local
 */
protected T initialValue() {
    return null;
}

很简单的返回了一个null,结合前面的源码,就是说默认会把当前ThreadLocal实例为key,null为value初始化到threadLocals中取,并且返回null值。

if the programmer desires thread-local variables to have an initial value other than null, ThreadLocal must be subclassed,我们也可以通过重写该方法,改变这个默认值。

4、remove

/**
 * Removes the current thread's value for this thread-local
 * variable.  If this thread-local variable is subsequently
 * {@linkplain #get read} by the current thread, its value will be
 * reinitialized by invoking its {@link #initialValue} method,
 * unless its value is {@linkplain #set set} by the current thread
 * in the interim.  This may result in multiple invocations of the
 * {@code initialValue} method in the current thread.
 *
 * @since 1.5
 */
 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

这个方法就是移除掉当前线程threadLocals中这个ThreadLocal实例对应的Entry,那么为什么需要有这个方法呢?

内存泄露,老生常谈的Java话题。当一个对象应该被释放掉但是还持有引用(GCRoot搜索),GC并不会释放掉这块内存,从而造成内存泄露。

5、弱引用

由于ThreadLocalMap.Entry继承自WeakReference的原因,穿插说说弱引用GC回收的情况;

String str = new String("hello");
WeakReference<String> entry  = new WeakReference<>(str);
System.out.println("before gc, " + entry.get());
System.gc();
Thread.sleep(1000);
System.out.println("after gc, " + entry.get());

结果是:

before gc, hello
after gc, hello

为什么这个entry引用的String对象没有被回收,那是因为不止entry这个弱引用指向了这个对象,str这个强引用也指向了它,根据GCRoot根搜索原则,它也不会被回收。

那么我们修改测试代码:

WeakReference<String> entry  = new WeakReference<>(new String("hello"));
System.out.println("before gc, " + entry.get());
System.gc();
Thread.sleep(1000);
System.out.println("after gc, " + entry.get());

结果是:

before gc, hello
after gc, null

印证了上面的分析。回到本文来说,如果在使用了ThreadLocal后,而没有手动执行remove方法,当这个线程迟迟没有结束,key 是弱引用,但是 value 无法被释放。

最典型的场景就是在线程池中使用 ThreadLocal,由于线程都是可缓存的,线程一直处在存活状况,每个线程 threadLocals 也会一直存在,那么所对应的Entry引用的对象空间除了 gc 也得不到释放。

所以,在每次使用 ThreadLocal 类时,最后记得使用remove方法显得尤为重要。

ThreadLocalMap 每次执行 get/set 都会进行自检操作 expungeStaleEntry,释放 key 等于 null 的 value 对象。

四、结束语

这是刨根问底系列的第一篇,从个人学习钻研源码的角度,一步一步地描述这个过程。相信不管是对于正在阅读这篇文章的你们,还是对于我自己,都是一个很好的学习和总结。

我个人偏向于洁癖,所以每一步粘贴的Java源码都原汁原味,带上官方的注释,有助于大家思考。除非万不得已,才略微插入几行注释加以说明。这也能在未来回顾这些原理时,能加以不同的思考,理解得更透彻。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容