写点什么

ThreadLocal 源码分析

  • 2023-04-24
    湖南
  • 本文字数:2771 字

    阅读完需:约 9 分钟

一、概述

ThreadLocal,即线程局部变量。主要用于线程间数据隔离。这些变量在多线程环境下访问(通过 get 或 set 方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal 实例通常来说都是 private static 类型。ThreadLocal 不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。


ThreadLocal 的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个 ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。

二、原理

ThreadLocal 如何实现线程独立访问 ThreadLocal 关联的变量呢?

这里主要有两种方式:

  1. 在 ThreadLocal 中维护一个 map,map 的 key 是线程,value 是关联的变量。但这种方式不太优雅(JDK1.5 之前采用的这种方式),比如说可能会导致线程很大,而且当线程销毁时,还需要在 map 中将其删除,在多线程情形下,会增加维护难度和时间成本。

  2. 每个 Thread 维护一个 ThreadLoaclMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object。这样做有很多好处,比如不用加锁来保证读写安全,而且当线程销毁时,与其关联的 ThreadLocalMap 也自然消亡。

ThreadLocalMap 的说明

ThreadLocal 没有直接使用 HashMap 而是自己重新开发了一个 map,最主要的作用是让他的 key 为虚引用类型,这样当 ThreadLocal 对象销毁时,多个持有其引用的线程不会影响它的回收。ThreadLocalMap 是一个很像 HashMap 的一个数据结构,但他并没有实现 Map 接口,而且它的 Entry 是继承 WeakReference 的,也没有 next 指针,所以不存在链表了。


对于 hash 冲突,而是采用的开放地址法来进行解决 ThreadLocaMap 的扩容机制也不同于 HashMap,ThreadLocalMap 的扩容阈值是长度的 2/3,当表中的元素数量达到阈值时,不会立即进行扩容,而是会触发一次 rehash 操作清除过期数据,如果清除过期数据之后元素数量大于等于总容量的 3/4 才会进行真正意义上的扩容。

// ThreadLocalMap没有继承Map接口static class ThreadLocalMap{    // Entry被声明为弱引用    static class Entry extends WeakReference<ThreadLocal<?>> {        /** The value associated with this ThreadLocal. */        Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }}
// 调用rehas方法private void rehash() { // 清除过期数据 expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis // 清除过期数据后数据仍然大于等于总容量的3/4,则扩容 if (size >= threshold - threshold / 4) resize();}
复制代码

get/set/初始化

当调用 get 或 set 方法时,首先会去检查线程的 ThreadLocalMap 是否被初始化,如果没有初始化,则会进行初始化操作,否则根据计算出来的 key 找到对应下标,如果对应下标是我们要找的元素,则返回,否则会向后查找,直到碰到 slot 为 null 或者找到为止。在这过程中同时会清理过期的 K-V 对,set 同理。具体可以参看源码。

// get方法public T get() {    // 获取当前线程类    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    // 判断是否初始化,如果没有则开始初始化    if (map != null) {        // 如果当前下标是目标元素,则返回,否则调用getEntryAfterMiss方法查找        // getEntryAfterMiss方法会继续查找并清理过期数据        ThreadLocalMap.Entry e = map.getEntry(this);        if (e != null) {            @SuppressWarnings("unchecked")                T result = (T)e.value;            return result;        }    }    return setInitialValue();}

// 初始化private T setInitialValue() { // 默认返回null,可以重新此方法决定每个线程在初始化map时获取的值 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } .... return value;}
// 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(); // 如果key相同,直接替换就行 if (k == key) { e.value = value; return; } // 过期数据,需要清理 // replaceStaleEntry函数逻辑:向后查找,直到遇到目标元素更新数据或桶位为空 // 插入值并清除过期数据 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 最后插入我们的目标元素 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}
复制代码

三、内存泄漏

强引用内存泄漏

如果 Entry 的 key 为强引用,则会导致 ThreadLocal 实例在被创建它的线程销毁时,而无法被回收,从而导致严重的内存泄漏问题,因此 Eetry 的 key 被声明为弱引用来避免这种问题

弱引用内存泄露

我们知道每一个线程都存在一个 ThreadLocalMap,Map 中的 key 为一个 ThreadLocal 实例。而且 key 到 ThreadLocal 实例的引用为虚引用,也就是说当 ThreadLocal 置为 null 时,没有任何强引用指向 ThreadLocal 实例,所以 ThreadLocal 实例会被 GC 回收。


但是 value 却不能被回收,因为存在一条从当前线程连接过来的强引用(Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value)针对上面的内存泄露问题,ThreadLocal 在 get 和 set 时都会检测并清除 key 为 null 的 Entry,从而尽可能的避免内存泄露

使用建议

  • 每次使用完 ThreadLocal,都调用它的 remove()方法,清除数据

  • 将 ThreadLocal 声明为 private static,使它的生命周期与线程保持一致


作者:code_writer

链接:https://juejin.cn/post/7211411281722867770

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
ThreadLocal源码分析_Java_做梦都在改BUG_InfoQ写作社区