深入理解 ThreadLocal

1.基础概念

ThreadLocal ,顾名思义就是用来提供线程(Thread)内部的局部(Local)变量的,主要应用场景为在同一个线程内方便地共享变量。例如:一次用户请求,服务器会为其开一个线程,我们在线程中创建一个 ThreadLocal,里面存请求上下文信息,这样在之后获取这些信息时就可以很方便地拿到,而不用层层显式传参。

2.实现原理

ThreadLocal 的实现原理并不复杂,核心就是在 Thread 类里维护了一个 Map:

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

然后通过 ThreadLocal 的接口读写这个变量:

/**
* 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;
}

/**
* 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);
}

至于 ThreadLocalMap,是一个专门为 ThreadLocal 应用场景定制的一个 HashMap,实现细节可以先不去管,Key 为 ThreadLocal 的实例,Value 为线程局部变量。所以说,ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

准备工作做好了,接下来看下 ThreadLocal 是怎么实现线程局部变量的读写的,核心代码也都很简单。

  • get:其实就是去读取 map 里的值。
/**
* 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();
}

当 map 为空或者没有读到值时,会触发初始化逻辑:

/**
* 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;
}
  • set:其实就是往 map 里面放值。
/**
* 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);
}
  • remove:用于使用完毕时清除数据,务必调用,不然会出现内存泄漏问题,见下文
/**
* 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;
        }
    }
}

3.内存泄漏问题

所谓内存泄漏,就是已经无法访问的内存却没有释放,ThreadLocal 使用不当时会导致内存泄漏问题。

首先需要理解使用 ThreadLocal 时的内存模型,看一下示意图:

ThreadLocal 运行时内存示意图

ThreadLocal.ThreadLocalMap的 Key 实现是弱引用,也即图中的虚线。弱引用不会阻止 GC,因此考虑下面的情况:

  1. ThreadLocalRef 被清除了,堆中的 ThreadLocal 实例不存在强引用了,被 GC 回收。

  2. ThreadLocalMap 里出现了一条 Key 为 null 的 Entry,后续无法读写,也无法回收,就造成了内存泄漏。

如果当前线程结束后被销毁,则这一块内存可以被释放;但是如果是线程池的模式,线程迟迟不结束的话,这个问题就会一直存在。

其实,ThreadLocalMap 的设计中已经考虑到这种情况,也加上了一些防护措施:在 ThreadLocal 的 get(), set(), remove() 的时候都会清除线程 ThreadLocalMap 里所有 key 为 null 的 value。

但这样也无法完全避免内存泄漏,因为可能上游再也不会调用get(),set(),remove()方法了,参考文章里也给出了一个 Tomcat 内存泄漏的实例:ThreadLocal 内存泄露的实例分析

正确的处理方式是:每次使用完 ThreadLocal,都调用它的 remove() 方法清除数据 ,这样才能从根源上避免内存泄漏问题。

4.应用实战

下面以在一次请求中透传上下文信息为例,来实际演示 ThreadLocal 用法。

首先创建一个类来管理 ThreadLocal 实例:

public class ContextInfoThreadLocal {

    private static final ThreadLocal<ContextInfo> CONTEXT_INFO_THREAD_LOCAL = new ThreadLocal<>();

    public static void set(ContextInfo contextInfo) {
        CONTEXT_INFO_THREAD_LOCAL.set(contextInfo);
    }

    public static ContextInfo get() {
        return CONTEXT_INFO_THREAD_LOCAL.get();
    }

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

然后在请求的入口处把上下文信息放进去,最好使用AOP的方式:

@Around("pointcut()")
    public Object around(ProceedingJoinPoint point) {
        try {
            ContextInfo ContextInfo = getContextInfo(point.getArgs());
            // 把 contextInfo 信息放入ThreadLocal
            ContextInfoThreadLocal.set(contextInfo);
            return point.proceed(point.getArgs());
        } catch (Throwable t) {
            // ...
        } finally {
            ContextInfoThreadLocal.remove(); // 记得清理ThreadLocal
        }
    }

有一点需要注意,因为 ThreadLocal 是线程局部变量,在一个线程里设置的值,只有在本线程内才可以获取,如果进行了线程切换,就无法拿到了。
但是实际应用中,为了提高接口性能,很多时候都会创建新线程进行一些并行处理,这时候如果想要在新线程中也能获取到上下文信息,就要在线程间传递 ContextInfo 了。
为了避免每次都写相同的代码,可以包装一下 Callable 来实现这个目的:

public class CallableWrapper<V> implements Callable<V> {
    private final ContextInfo contextInfo;

    private final Callable<V> task;

    public CallableWrapper(ContextInfo contextInfo, Callable<V> task) {
        this.contextInfo = contextInfo;
        this.task = task;
    }

    @Override
    public V call() throws Exception {
        ContextInfoThreadLocal.set(contextInfo);
        try {
            return task.call();
        } finally {
            ContextInfoThreadLocal.remove();
        }
    }
}

5.参考文章

https://blog.xiaohansong.com/ThreadLocal-memory-leak.html

https://juejin.im/post/5ba9a6665188255c791b0520

http://www.majiang.life/blog/the-smart-way-of-passing-parameter-by-threadlocal/

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

推荐阅读更多精彩内容