ThreadLocal源码解读以及如何正确使用

201 阅读5分钟

ThreadLocal是什么?

ThreadLocal是JDK原生为我们提供的可保存线程隔离的变量的容器。
本身ThreadLocal变量是保存在Thread类的ThreadLocalMap类型的成员变量中的,且ThreadLocal变量本身作为ThreadLocalMap的key(这里可能很多人有误区,我见过很多人认为是ThreadLocal类内部保存了一个map) image.png 下面给出Thread、ThreadLocal、ThreadLocalMap三者之间的引用关系图 这个图很关键,掌握了这个图关于ThreadLocal的引用问题就已经完全掌握了

image.png 在许多中间件以及框架中都有应用,比如Spring Tx中就是通过ThreadLocal来保存事务相关的信息。 可以自行查看TransactionSynchronizationManager,这里不再赘述。
有些同学可能纳闷在某些情况下ThreadLocal的作用是否可以通过方法变量入参来实现,确实可以,但是有多个缺陷

  • 需要在整个方法调用链中传递这个变量
  • 整体的调用链路由自己一个人把控,否则就要要求别人接口设计多带一个入参

ThreadLocal源码

ThreadLocal的源码其实很简单明了
主要涉及ThreadLocal、ThreadLocalMap、Thread三个类
了解ThreadLocal源码,一般只需要关注三个关键方法即set、get、remove

set

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

可以看到主要分三步

  1. 获取当前线程
  2. 获取当前线程的成员变量threadLocals (ThreadLocalMap类型)
  3. 通过操作ThreadLocalMap的set方法进行值的设置/初始化ThreadLocalMap

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

get方法和set方法流程几乎一致

  1. 获取当前线程
  2. 获取当前线程的成员变量threadLocals (ThreadLocalMap类型)
  3. 通过ThreadLocalMap获取值/设置并返回默认值

remove

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    }
}
  1. 获取当前线程
  2. 获取当前线程的成员变量threadLocals (ThreadLocalMap类型)
  3. 调用ThreadLocalMap的remove方法

ThreadLocalMap

通过上述ThreadLocal中的源码可以知道
其实操作的核心是threadLocals线程成员变量,即ThreadLocalMap类的相关方法
ThreadLocalMap作为ThreadLocal类的静态内部类,使用Entry[] table数组实现了Map的相关操作

Entry

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

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

Entry继承了WearRefrence类,将key作为弱引用存在,其value就是我们通过ThreadLocal.set()方法set进来的值,为强引用。

set方法

本文只重点介绍set方法

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    // 上面就是通过hash值以及位运算计算下标位置
    // 因为ThreadLocal是通过开放寻址法来消除hash冲突
    // 所以会一直遍历到entry为null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        // 如果key已存在 覆盖value
        if (k == key) {
            e.value = value;
            return;
        }
        // 遍历到key由于弱引用被回收 也会创建entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 找到entry为null的下标 创建entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 后续清理key被回收的entry
    // 以及判断是否需要扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocal的设计与可能的坑

ThreadLocalMap为何将key设计为弱引用

首先先回顾下java的四种引用

  • 强引用:一般没有特别申明的对象都是强引用。这种对象只有在GCroots找不到它的时候才会被回收。
  • 软引用(SoftReference的子类):GC后内存不足的情况将只有这种引用的对象回收。
  • 弱引用(WeakReference的子类): GC时回收只有此引用的对象(无论内存是否不足)。
  • 虚引用(PhantomReference子类):没有特别的功能,类似一个追踪符,配合引用队列来记录对象何时被回收 为什么设计为弱引用,网上一般说法是为了避免内存泄漏 我们来看看怎么避免的内存泄漏
image.png

弱引用可以斩断entry->ThreadLocal这一条引用链,从而可以在ThreadLocalRef=null的时候,将ThreadLocal实例回收掉。 如果没有设置弱引用的话,ThreadLocal实例就有可能导致内存泄漏,因为获取不到他了,remove都没法执行 这里额外提一嘴,一般规范ThreadLocal变量是作为static final类型的,所以即使斩断这一条引用链,ThreadLocal实例也不会被回收

一个问题,为什么value不能设置为弱引用?

因为value只有一条entry->Value的引用链,如果value弱引用,值可能被提前回收;我们就有可能获取不到值了,ThreadLocal存在的意义也就没了
而key还存在ThreadLocalRef这一个引用链,所以如果不是手动赋值ThreadLocalRef=null,是不会被回收的

ThreadLocal内存泄漏

在网上一直流传着ThreadlLocal的不规范使用会造成内存泄漏问题,让我们来分析下问题来源 通过上面的引用图我们可以知道,entry.Value会被强引用,也就是只有他才会造成内存泄漏 但其实条件还蛮苛刻的

  • 首先要有线程池这种线程不会销毁的场景
  • 没有调用set/get/remove方法 其实ThreadLocal内部虽然有惰性清理方法expungeStaleEntry,但还是建议在使用完后主动调用remove方法

线程重用数据混乱

在线程池场景下,如果没有做好清理工作,可能会导致下一个任务获取数据错乱 解决方法依然是注意在使用完成后调用remove方法

综上所述,其实只需要注意显示清理ThreadLocal,即调用其remove方法即可避免落入ThreadLocal的坑里