如何正确使用 ThreadLocal

news2024/11/25 18:50:17

1 前言

当多线程访问共享且可变的数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要ThreadLocal出场了。

ThreadLocal又称线程本地变量,使用其能够将数据封闭在各自的线程中,每一个ThreadLocal能够存放一个线程级别的变量且它本身能够被多个线程共享使用,并且又能达到线程安全的目的,且绝对线程安全,其用法如下所示:


public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();

RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

除了线程安全之外,使用ThreadLocal也能够作为一种“方便传参”的工具,在业务逻辑冗长的代码中,同一个参数需要传入在多个方法之间层层传递,当这种需要传递的参数过多时代码会显得十分臃肿、丑陋;

之前我给公司做过企微会话存档的功能,就是将企业微信聊天信息拉取下来保存,由于企业微信消息类型很多(至少有三十多种),为了后期便于维护在对消息解析、保存时根据消息类型封分别封装了对应的方法每个消息类型解析、保存时又会进一步细分拆分成多个方法(比如说文件资源的分片拉取、上传到静态资源服务器),这个时候麻烦的事情就来了,每个方法的入参都需要包含企微会话存档的相关配置参数和封装的对话信息参数,导致入参列表非常长,阅读性比较差。

实际上可以把企微会话存档的相关配置参数存入到ThreadLocal中,各个方法内需要使用直接从ThreadLocal中获取就可以了,以后有时间了要把这块代码重构一下。

后来我又做了公司的短信模块的需求,主要是记录短信发送记录、发送统计及短信发送状态,短信发送的接口有多个(单条发送、批量发送、根据模板发送等等),需要记录多个接口的调用情况,当时就抽象出了短信上下文、模板上下文等实体,在调用方法时首先构造对应的上下文并将其保存到ThreadLocal中,在短信余额校验、违禁词过滤、余额不足提醒等业务处理方法中只需要从ThreadLocal中取出对应的上下文即可,而且发送状态是通过切面进行记录的,在切入点记录日志时也是直接从ThreadLocal中直接获取的上下文信息,代码简洁、可读性高。

说了不少废话,现在就步入正题了,让我们揭开ThreadLocal的庐山真面目。

2 原理

2.1 重点方法

先看一下ThreadLocal的结构

在这里插入图片描述

需要我们重点关注的方法有:

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

如果能够搞懂这块代码,就能够明白ThreadLocal到底是怎么实现的了。这块代码其实很有意思,我们发现在向ThreadLocal中存放值时需要先从当前线程中获取ThreadLocalMap,最后实际是要把当前ThreadLocal对象作为key、要存入的值作为value存放到ThreadLocalMap中,那我们就不得不先看一下ThreadLocalMap的结构。


static class ThreadLocalMap {
    /**
     * 键值对实体的存储结构
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // 当前线程关联的 value,这个 value 并没有用弱引用追踪
        Object value;

        /**
         * 构造键值对
         *
         * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
         * @param v v 作 value
         */
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    // 初始容量,必须为 2 的幂
    private static final int INITIAL_CAPACITY = 16;

    // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
    private Entry[] table;

    // ThreadLocalMap 元素数量
    private int size = 0;

    // 扩容的阈值,默认是数组大小的三分之二
    private int threshold;
}

ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal,从源码中看到 ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal内存入 的值。

ThreadLocalMap 解决 hash 冲突的方式采用的是「线性探测法」,如果发生冲突会继续寻找下一个空的位置。

每个Thread内部都持有一个ThreadLoalMap对象

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

至此,我们都能够明白ThreadLocal存值的过程了,虽然我们是按照前言中的用法声明了一个全局常量,但是这个常量在每次设置时实际都是向当前线程的ThreadLocalMap内存值,从而确保了数据在不同线程之间的隔离。

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

有了上面的铺垫,这段代码就不难理解了,获取ThreadLocal内的值时,实际上是从当前线程的ThreadLocalMap中以当前ThreadLocal对象作为key取出对应的值,由于值在保存时时线程隔离的,所以现在取值时只会取得当前线程中的值,所以是绝对线程安全的。

2.1.3 remove


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

remove将ThreadLocal对象关联的键值对从Entry中移除,正确执行remove方法能够避免使用ThreadLocal出现内存泄漏的潜在风险,int i = key.threadLocalHashCode & (len-1)这行代码很有意思,从一个集合中找到一个元素存放位置的最简单方法就是利用该元素的hashcode对这个集合的长度取余,如果我们能够将集合的长度限制成2的整数次幂就能够将取余运算转换成hashcode与[集合长度-1]的与运算,这样就能够提高查找效率,HashMap中也是这样处理的,这里就不再展开了。

下面的一张图很好的解释了ThreadLocal的原理

在这里插入图片描述

3 ThreadLocal内存泄漏及正确用法

在提及ThreadLocal使用的注意事项时,所有的文章都会指出内存泄漏这一风险,但是我发现很少有文章能够真正的把这一部分讲清楚,这里我就斗胆尝试一下,由于ThreadLocalMap中的Entry的key持有的是ThreadLocal对象的弱引用,当这个ThreadLocal对象当且仅当被ThreadLocalMap中的Entry引用时发生了GC,会导致当前ThreadLocal对象被回收;那么 ThreadLocalMap 中保存的 key 值就变成了 null,而Entry 又被 ThreadLocalMap 对象引用,ThreadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不销毁的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。

下面我们就来验证一下这个情景,我们在方法内部声明了一个ThreadLocal对象,为了更好的演示内存泄漏的情景我们在使用这个对象存值后将方法内取消对其的强引用,并且通过System.gc()触发了一次垃圾回收(准确的说是希望jvm执行一次垃圾回收,不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的),这样再垃圾回收时会将ThreadLocal对象回收,代码如下所示:


	@Test
    public void loop() throws Exception {

        for (int i = 0; i < 1; i++) {
            ThreadLocal<SysUser> threadLocal = new ThreadLocal<>();
            threadLocal.set(new SysUser(System.currentTimeMillis(), "李四"));
           // threadLocal = null;
            //System.gc();
            printEntryInfo();
        }

        //System.gc();

        //printEntryInfo();
    }

    private void printEntryInfo() throws Exception {
        Thread currentThread = Thread.currentThread();
        Class<? extends Thread> clz = currentThread.getClass();
        Field field = clz.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object threadLocalMap = field.get(currentThread);
        Class<?> tlmClass = threadLocalMap.getClass();
        Field tableField = tlmClass.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] arr = (Object[]) tableField.get(threadLocalMap);
        for (Object o : arr) {
            if (o != null) {
                Class<?> entryClass = o.getClass();
                Field valueField = entryClass.getDeclaredField("value");
                Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                valueField.setAccessible(true);
                referenceField.setAccessible(true);
                System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
            }
        }
    }

在不发生GC时,控制台输出如下:

在这里插入图片描述
ThreadLocal对象并未被回收,将System.gc();放开,控制台输入如下:

在这里插入图片描述
可以看出key确实变成了null值,而Entry内会一直持有对value的引用,导致value无法被回收,如果当前线程一直在执行未被销毁,则确实会出现内存泄漏(在使用线程池时更容易出现这样的问题)。

让我们分析一下上面的为什么会出现内存泄漏的原因,在上面的代码里,我们在方法内部声明了一个ThreadLocal对象,该ThreadLocal对象仅有一个方法内部的强引用且的生命周期很短,当该方法执行完成之后此ThreadLocal对象在下一次gc时就会被回收,当然我们可以在方法结束前手动执行一个该对象的remove方法,但是这样就失去了使用ThreadLocal的意义。

由此,我们知道出现内存泄漏的原因是失去了对ThreadLocal对象的强引用,避免内存泄漏最简单的方法就是始终保持对ThreadLocal对象的强引用,为每个线程声明一个对ThreadLocal对象的强引用显然是不合适的(太麻烦且缺乏声明的时机),所以,我们可以将ThreadLocal对象声明为一个全局常量,所有的线程均使用这一常量即可,例如:


private static final ThreadLocal<String> RESOURCE = new ThreadLocal<>();

    @Test
    public void multiThread() {
        Thread thread1 = new Thread(() -> {
            RESOURCE.set("thread1");
            System.gc();
            try {
                printEntryInfo();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(() -> {
            RESOURCE.set("thread2");
            System.gc();
            try {
                printEntryInfo();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        thread2.start();
    }

按照上面的方式声明ThreadLocal对象后,所有的线程共用此对象,在使用此对象存值时会把此对象作为key然后把对应的值作为value存入到当前线程的ThreadLocalMap中,由于此对象始终存在着一个全局的强引用,所以其不会被垃圾回收,调用remove方法后就能够将此对象关联的Entry清除。

验证一下:


弱引用key:java.lang.ThreadLocal@10c6d8a7,:thread1
弱引用key:java.lang.ThreadLocal@10c6d8a7,:thread2

可以看出两个线程内对应的Entry的key为同一个对象且即使发生了垃圾回收该对象也不会被回收。

那么是不是说将ThreadLocal对象声明为一个全局常量后使用就没有问题了呢,当然不是,我们需要确保在每次使用完ThreadLocal对象后确保要执行一下该对象的remove方法,清除当前线程保存的信息,这样当此线程再被利用时不会取到错误的信息(使用线程池极易出现);

我们的项目之前就出现过这种场景,从线程池中获取线程,并在每次请求时在当前线程记录下对应的用户信息,结果有一天出现了串号的问题,B用户访问时使用了A用户的信息,这就是在每次请求结束后没有执行remove方法,线程复用时内部还保存着上一个用户的信息,贴上一份使用ThreadLocal的正确姿势:


package com.cube.share.thread.config;

import com.cube.share.thread.entity.SysUser;

/**
 * @author poker.li
 * @date 2021/7/31 14:50
 * <p>
 * 线程当前用户信息
 */
public class CurrentUser {

    private static final ThreadLocal<SysUser> USER = new ThreadLocal<>();

    private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();

    private static final InheritableThreadLocal<SysUser> INHERITABLE_USER = new InheritableThreadLocal<>();

    private static final InheritableThreadLocal<Long> INHERITABLE_USER_ID = new InheritableThreadLocal<>();

    public static void setUser(SysUser sysUser) {
        USER.set(sysUser);
        INHERITABLE_USER.set(sysUser);
    }

    public static void setUserId(Long id) {
        USER_ID.set(id);
        INHERITABLE_USER_ID.set(id);
    }

    public static SysUser user() {
        return USER.get();
    }

    public static SysUser inheritableUser() {
        return INHERITABLE_USER.get();
    }

    public static Long inheritableUserId() {
        return INHERITABLE_USER_ID.get();
    }

    public static Long userId() {
        return USER_ID.get();
    }

    public static void removeAll() {
        USER.remove();
        USER_ID.remove();
        INHERITABLE_USER.remove();
        INHERITABLE_USER_ID.remove();
    }
}

我们可以通过切面或者请求监听器在请求结束时将当前线程保存的ThreadLocal信息清除


/**
 * @author poker.li
 * @date 2021/7/31 15:12
 * <p>
 * ServletRequest请求监听器
 */
@Component
@Slf4j
public class ServletRequestHandledEventListener implements ApplicationListener<ServletRequestHandledEvent> {

    @Override
    public void onApplicationEvent(ServletRequestHandledEvent event) {
        CurrentUser.removeAll();
        log.debug("清除当前线程用户信息,uri = {},method = {},servletName = {},clientAddress = {}", event.getRequestUrl(),
                event.getMethod(), event.getServletName(), event.getClientAddress());
    }
}

4 可传递给子线程的InheritableThreadLocal

如果我们在当前线程中开辟新的子线程并希望子线程获取父线程保存的线程本地变量要怎么做呢,在子线程中声明ThreadLocal对象并将父线程中对应的值存入自然是可以的,但是大可不必如此繁琐,jdk已经为我们提供了一种可传递给子线程的InheritableThreadLocal,实现的原理也很简单,可以在Thread中一窥究竟。


//持有了一个可传递给子线程的ThreadLocalMap
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

//线程创建时都会执行这个初始化方法,inheritThreadLocals表示是否需要在构造时从父线程中继承thread-locals,默认为true
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        //忽略了一部分代码

        setPriority(priority);
        //从父线程中继承thread-locals
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

5 使用场景

ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:

  • 线程间数据隔离,各线程的 ThreadLocal 互不影响

  • 方便同一个线程使用某一对象,避免不必要的参数传递

  • 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal

  • Spring 事务管理器采用了 ThreadLocal

  • Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal

6 总结

本文主要从源码的角度解析了 ThreadLocal,并分析了发生内存泄漏的原因及正确用法,最后对它的应用场景进行了简单介绍。

ThreadLocal还有其他变种例如FastThreadLocal和TransmittableThreadLocal,FastThreadLocal主要解决了伪共享的问题比ThreadLocal拥有更好的性能,TransmittableThreadLocal主要解决了线程池中线程复用导致后续提交的任务并不会继承到父线程的线程变量的问题,这里限于篇幅就不展开了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/724849.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据管理成熟度评估DCMM之生产企业数据战略管理办法

生产企业数据战略管理办法 第一部分&#xff1a;导言 随着信息技术的快速发展和数据规模的急剧增长&#xff0c;生产企业越来越重视数据的价值和管理。有效的数据战略管理办法可以帮助生产企业更好地管理和利用数据资源&#xff0c;提高运营效率、决策质量和创新能力。本文将…

SpringMVC数据传递总结

文章目录 1. 分析总结2. 普通格式数据2.1 普通参数2.2 pojo参数2.3 嵌套pojo参数2.4 数组 -- 普通参数2.5 集合 -- 普通参数2.6 web容器添加过滤器指定字符集 3. JSON格式数据3.1 相关准备3.2 json数组(基本)3.3 json对象(pojo)3.4 json数组(pojo) 1. 分析总结 1.1 普通格式数据…

K8S平台安全框架

平台安全框架 1 平台安全框架1.1 安全框架1.1.1 认证框架1.1.2 框架解读 1.2 认证实践1.2.1 令牌用户1.2.2 证书用户 1.3 授权实践1.3.1 集群用户1.3.2 角色基础1.3.3 授权基础1.3.4 用户组实践1.3.5 SA授权1.3.6 SA秘钥 1.4 准入实践1.4.1 准入基础1.4.2 优先调度1.4.3 资源配…

F2-NeRF阅读日志

看到了一篇很好的paper&#xff0c;记录一下&#xff0c;参考&#xff1a; https://www.bilibili.com/video/BV1Lz4y187jL/?spm_id_from333.337.search-card.all.click&vd_sourcea059a118f33728f79abd79e02f8f72d4 https://zhuanlan.zhihu.com/p/618362291 latex写的&am…

Qt5编译使用QFtp模块(环境:win+Qt5.15.2+msvc2019)

目录 QFtp下载编译配置QFtp模块测试 QFtp下载 下载方式较多&#xff0c;可以从github上进行下载&#xff1a;https://github.com/qt/qtftp.git 。 我已将下载好的ftp源码资源放出来了&#xff0c;可以直接下载0积分&#xff1a;链接跳转。 编译 使用Qt Create打开工程后&…

DuDuTalk:4G录音工牌在汽车试乘试驾场景中有什么独特应用价值?

在市场竞争越来越激烈的今天&#xff0c;不管是新能源市场还是燃油车市场&#xff0c;试乘试驾已经当仁不让地成为了几乎所有汽车品牌关注的焦点。特斯拉、“蔚小理”、奔驰、宝马等头部品牌&#xff0c;对于试乘试驾的重视度一定程度上甚至已经超过了展厅接待。 然而&#xf…

解决notion共享网址无法复制的问题

1、打开url Notion – The all-in-one workspace for your notes, tasks, wikis, and databases. 2、选中要复制的内容。 3、右击鼠标&#xff0c;选择“打印” 4、在打印界面中选中要复制的内容&#xff0c;然后按“复制” 复制完成。

Stable Difussion能做什么?

​扩散模型&#xff08;Diffusion Model&#xff09;​ 稳定扩散模型&#xff08;Stable Diffusion&#xff09;属于深度学习模型中的一个大类&#xff0c;即扩散模型。它们属于生成式模型&#xff0c;这意味着它们是被设计用于根据学习内容来生成相似的新的数据的。对于稳定扩…

Vue2与Vue3相应原理区别

Vue3.0中的响应式原理 vue2.x的响应式 1.实现原理&#xff1a; 对象类型&#xff1a;通过Object.defineProperty()对属性的读取、修改进行拦截&#xff08;数据劫持&#xff09;。数组类型&#xff1a;通过重写更新数组的一系列方法来实现拦截。&#xff08;对数组的变更方法…

测试老鸟整理,性能测试高并发压力测试-案例,进阶之道...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 单个接口的压测&a…

通过foxmail同步其他邮箱邮件到我司邮箱

1、先通过foxmail 登录需要备份邮件的邮箱帐号&#xff0c;全选需要备份的邮件&#xff0c;右键选择“导出邮件”。 2、在foxmail中使用IMAP协议登录我司邮箱帐号&#xff0c;右键选择收件箱或其他文件夹导入邮件&#xff0c;将之前导出备份的邮件文件全选导入。 3、导入完成后…

glibc缺陷居然会导致MySQL卡住?

问题来源&#xff1a; 版本&#xff1a;5.7.25。 现象&#xff1a;备机主从延迟不断变大&#xff0c;无法登陆数据库&#xff0c;建立连接时卡住&#xff0c;但很快恢复正常了。 分析&#xff1a; 常规分析&#xff1a; 通常情况下&#xff0c;这类问题无法分析&#xff0c…

vmware17安装openkylin

官网 系统下载-openKylin 开放麒麟社区官网 | 开源聚力&#xff0c;共创未来 下载链接 https://www.openkylin.top/downloads/download-smp.php?id18 安装 点击浏览&#xff0c;选择镜像 修改服务器cpu配置 修改内存配置 修改网络连接方式 点击启动 等待安装完成 出现上图说…

比较两个Excel表格中的数据,不相同的高亮显示

下面是常用的在Excel中比较两个Excel表格数据的方法&#xff0c; 比如要比较下面A和B中的数据是否一致&#xff1a; 可以这样做&#xff1a;

ikbc键盘2.4G接收器丢失,重新对码

我的键盘&#xff1a;ikbc W200 1.键盘关掉重开&#xff1b; 2.新接收器插在电脑上&#xff1b; 3.电脑上打开软件&#xff0c;点开始对码&#xff0c;一会就连接上了。 对码软件放在这里&#xff1a; 我用夸克网盘分享了「IKBC 对码.rar」&#xff0c;点击链接即可保存。打开…

(Qt) Http之Get请求使用基础

文章目录 前言Code完整code效果循环loop的使用 讲解配置相关对象Https END 前言 http的操作是网络通信中非常常见的操作&#xff0c;其中广泛使用get&#xff0c;post两种操作。 本文将对Qt中&#xff0c;http的get请求做简单应用&#xff0c;来进行展示。 Code 完整code …

2分钟完成Github配置ssh密钥

2分钟完成Github配置ssh密钥 前言&#xff1a; 配置ssh密钥的目的是为了可以在自己的本地开发环境顺利拉取和推送代码到Github 本机是Ubuntu 16.04环境&#xff0c;已安装ssh 步骤1 进入当前用户的home目录&#xff1a; #执行以下命令进入 cd ~进入.ssh/目录下生成ssh的密钥…

JS antdv实现表格树形级联效果

概述 项目中需实现以下这种效果&#xff1a;级联数据&#xff0c;表格横向排列&#xff0c;数据之间相互联动。现有UI组件无法满足此需求&#xff0c;只能撸起袖子加油干&#xff01;&#xff01; 实现效果如下 开发前先准备一个树形数据 treeData.js const treeData [{ar…

Linux常用命令【多图预警】

Linux常用命令 文章目录 Linux常用命令Linux虚拟机的安装Linux系统目录结构Linux命令的语法基础命令查看当前目录下所有子目录和子文件ls查看命令手册man查看当前目录pwd切换到指定目录cd 管道符 |文件目录操作命令创建一级目录&#xff08;文件夹&#xff09;mkdir创建多级目录…

计算机毕业论文内容参考|基于微信小程序的学生课表系统的设计与实现

文章目录 导文摘要前言绪论1课题背景2国内外现状与趋势3课题内容相关技术与方法介绍系统分析系统设计系统实现系统测试总结与展望1本文总结2后续工作展望导文 计算机毕业论文内容参考|基于微信小程序的学生课表系统的设计与实现 摘要 基于微信小程序的学生课表系统是一个方便学…