你是否线上有使用ThreadLocal,如果结合多线程,请慎用

news2024/11/25 3:37:28

随着业务的增加,数据量的增加,多线程的使用会越来越频繁,提升单机的处理能力。

前些日子我们线上出现了一个比较严重的故障,这个故障是多线程使用不当引起的,挺有代表性的,所以分享给大家,希望能帮大家避坑。

问题简述

先简单介绍一下问题产生的背景,我们有个返利业务,其中有个搜索场景,这个场景是用户在 app 输入搜索关键词,然后 server 会根据这个关键词到各个平台(如淘宝,京东,拼多多等)调一下搜索接口,聚合这些搜索结果后再返回给用户,最开始这个搜索场景处理是单线程的,但随着接入的平台越来越多,搜索请求耗时也越来越长,由于每个平台的搜索请求都是独立的,很显然,单线程是可以优化为多线程的,如下

 

这样的话,搜索请求的耗时就只取决于搜索接口耗时最长的那个平台,所以使用多线程显然对接口性能是一个极大的优化,但使用多线程改造上线后,短时间内社群中有多名用户反馈前台展示「APP 需要升级的提示」,经定位后发现是因为在多线程中无法获取客户端信息,由于客户端信息缺失,导致返回给用户需要升级的提示,伪代码如下

// 开启多线程处理
new Thread(new Runnable() {
    @Override
    public void run() {
        Map clientInfoMap = Context.getContext().getClientInfo();
          // 无法获取客户端信息,返回需要升级的信息
          if (clientInfoMap == null) {
            throw new Exception("版本号过低,请升级版本");
        }
        String version = clientInfoMap.get("version");


        // 以下正常逻辑
        ....
    }
}).start();

画外音:在生产中多线程使用的是线程池来实现,这里为了方便演示,直接 new Thread,效果都一样,大家知道即可

那么问题来了,改成多线程后客户端信息怎么就取不到了呢?要搞清楚这个问题,就得先了解客户端信息是如何存储的了

Threadlocal 简介

不同客户端请求的客户端信息(wifi 还是 4G,机型,app名称,电量等)显然不一样,dubbo 业务线程拿到客户端请求后首先会将有用的请求信息提取出来(如本文中的 Map clientInfo),但这个 clientInfo 可能会在线程调用的各个方法中用到,于是如何存储就成为了一个现实的问题,相信有经验的朋友一下就想到了,没错,用 Threadlocal !为什么用它,它有什么优势,简单来说有两点

  1. 无锁化提升并发性能

  2. 简化变量的传递逻辑

1.无锁化提升并发性能

先说第一个,无锁化提升并发性能,影响并发的原因有很多,其中一个很重要的原因就是锁,为了防止对共享变量的竞用,不得不对共享变量加锁

如果对共享变量争用的线程数增多,显然会严重影响系统的并发度,最好的办法就是使用“影分身术”为每个线程都创建一个线程本地变量,这样就避免了对共享变量的竞用,也就实现了无锁化

无锁化

ThreadLocal 即线程本地变量,它可以为每个线程创建一份线程本地变量,使用方法如下

static ThreadLocal<SimpleDateFormat> threadLocal1 = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd");
    }
};

public String formatDate(Date date) {
    return threadLocal1.get().format(date);
}

这样的话每个线程就独享一份与其他线程无关的 SimpleDateFormat 实例副本,它们调用 formatDate 时使用的 SimpleDateFormat 实例也是自己独有的副本,无论对副本怎么操作对其他线程都互不影响

通过以上例子我们可以看出,可以通过 new ThreadLocalinitialValue 来为创建的 ThreadLocal 实例初始化本地变量(initialValue 方法会在首次调用 get 时被调用以初始化本地变量)。当然,如果之后需要修改本地变量的话,也可以用以下方式来修改

threadLocal1.set(new SimpleDateFormat("yyyy-MM-dd"))

而使用 threadLocal1.get()这样的方法即可获得线程本地变量

可能一些朋友会好奇线程本地变量是如何存储的,一图胜千言

每一个线程(Thread)内部都有一个 ThreadLocalMap, ThreadLocal 的 get 和 set 操作其实在底层都是针对 ThreadLocalMap 进行操作的

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

它与 HashMap 类似,存储的都是键值对,只不过每一项(Entry)中的 key 为 threadlocal 变量(如上文案例中的 threadLocal1),value 才为我们要存储的值(如上文中的 SimpleDateFormat 实例),此外它们在碰到 hash 冲突时的处理策略也不同,HashMap 在碰到 hash 冲突时采用的是链表法,而 ThreadLocalMap 采用的是线性探测法

2.简化变量的传递逻辑

接下来我们来看使用 ThreadLocal 的等二个好处,简化变量的传递逻辑,线程在处理业务逻辑时可能会调用几十个方法,如果这些方法中只有几个需要用到 clientInfo,难道要在这几十个方法中定义一个 clientInfo 参数来层层传递吗,显然不现实。那该怎么办呢,使用 ThreadLocal 即可解决此问题。由上文可知通过 ThreadLocal 设置的本地变量是同 threadlocal 一起保存在 Thread 的 ThreadLocalMap 这个内部类中的,所以可在线程调用的任意方法中取出,伪代码如下

public class ThreadLocalWithUserContext implements Runnable {

    private static ThreadLocal<Map<String,String>> threadLocal 
      = new ThreadLocal<>();

    @Override
    public void run() {
                // clientInfo 初始化
        Map<String, String> clientInfo = xxx;
        threadLocal.set(clientInfo);
          test1();
    }

      public void test1() {
                test2(); 
    }

    public void test2() {
        testX();
    }
      ...

    public void testX() {
                Map clientInfo = threadLocal.get();
    }
}

中间定义的任何方法都无需为了传递 clientInfo 而定义一个额外的变量,代码优雅了不少

由以上分析可知,使用 ThreadLocal 确实比较方便,在此我们先停下来思考一个问题:如果线程在调用过程中只用到一个 clientInfo 这样的信息,只定义一个 ThreadLocal 变量当然就够了,但实际上在使用过程中我们可能要传递多个类似 clientInfo 这样的信息(如 userId,cookie,header),难道因此要定义多个 ThreadLocal 变量吗,这么做不是不可以,但不够优雅,更合适的做法是我们只定义一个 ThreadLocal 变量,变量存的是一个上下文对象,其他像 clientInfo,userId,header 等信息就作为此上下文对象的属性即可,代码如下

public final class Context {

    private static final ThreadLocal<Context> LOCAL = new ThreadLocal<Context>() {
        protected Context initialValue() {
            return new Context();
        }
    };


      private Long uid;     // 用户uid
      private Map<String, String> clientInfo; // 客户端信息
      private Map<String, String> headers = null; // 请求头信息
      private Map<String, Map<String, String>> cookies = null; // 请求 cookie

      public static Context getContext() {
        return (Context) LOCAL.get();
    }

}

这样的话我们可通过 Context.getContext().getXXX() 的形式来获取线程所需的信息,通过这样的方式我们不仅避免了定义无数 ThreadLocal 变量的烦恼,而且还收拢了上下文信息的管理

通过以上介绍相信大家也都知道了 clientInfo 其实是借由 ThreadLocal 存储的,认清了这个事实后那我们现在再回头看开头的生产问题:将单线程改成多线程后,为什么在新线程中就拿不到 clientInfo 了?

问题剖析

源码之下无秘密,我们查看一下源码来一探究竟,获取本地变量的值使用的是 ThreadLocal.get 方法,那就来看下这个方法

public class ThreadLocal<T> {
        public T get() {
        // 1.先获取当前线程
        Thread t = Thread.currentThread();
          // 2.再获取当前线程的 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
}

可以看到 get 方法主要步骤如下

  1. 首先需要获取当前线程

  2. 其次获取当前线程的 ThreadLocalMap

  3. 进而再去获取相应的本地变量值

  4. 如果没有的话则调用 initiaValue 方法来初始化本地变量

由此可知当我们调用 threadlocal.get  时,会拿到当前线程的 ThreadLocalMap,然后再去拿 entry 中的本地变量,而对多线程来说,新线程的 ThreadLocalMap 里面的东西本来就未做任何设置,是空的,拿不到线程本地变量也就合情合理了

解决方案

问题清楚了,那怎么解决呢,不难得知主要有两种方案

1.我们之前是在新线程的执行方法中调用 threadlocal.get 方法,可以改成先从当前执行线程中调用 threadlocal.get 获得 clientInfo,然后再把 clientInfo 传入新线程,伪代码如下

// 先从当前线程的 Context 中获取 clientInfo
Map clientInfoMap = Context.getContext().getClientInfo();
new Thread(new Runnable() {
    @Override
    public void run() {
                // 此时的 clientInfoMap 由于是在新线程创建前获取的,肯定是有值的
        String version = clientInfoMap.get("version");


        // 以下正常逻辑
        ....
    }
}).start();

2.只需把 ThreadLocal 换成 InheritableThreadLocal,如下

public final class Context {
    private static final InheritableThreadLocal<Context> LOCAL = new InheritableThreadLocal<Context>() {
        protected Context initialValue() {
            return new Context();
        }
    };

      public static Context getContext() {
        return (Context) LOCAL.get();
    }
}

new Thread(new Runnable() {
    @Override
    public void run() {
                // 此时的 clientInfo 能正常获取到
        Map clientInfo = Context.getContext().getClientInfo();
        String version = clientInfo.get("version");
        // 以下正常逻辑
        ....
    }
}).start();

为什么 InheritableThreadLocal 能有这么神奇,背后的原理是什么?

由前文介绍我们得知,ThreadLocal 变量最终是存在 ThreadLocalMap 中的,那么能否在创建新线程的时候,把当前线程的 ThreadLocalMap 复制给新线程的 ThreadLocalMap 呢,这样的话即便你从新线程中调用 threadlocal.get 也照样能获得对应的本地变量,和 InheritableThreadLocal 相关的底层干的就是这个事,我们先来瞧一瞧 InheritableThreadLocal 长啥样

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

由此可知 InheritableThreadLocal 其实是继承自 ThreadLocal 类的,此外我们在 getMap 和 createMap 这两个方法中也发现它的底层其实是用 inheritableThreadLocals 来存储的,而 ThreadLocal 用的是 threadLocals 变量存储的

public class Thread implements Runnable {
    // ThreadLocal 实例的底层存储
    ThreadLocal.ThreadLocalMap threadLocals = null;

      // inheritableThreadLocals 实例的底层存储
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

知道了这些,我们再来看下创建线程时涉及到的 inheritableThreadLocals 复制相关的关键代码如下:

public class Thread implements Runnable {
    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
          ...
          Thread parent = currentThread();
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
              // 将当前线程的 inheritableThreadLocals 复制给新创建线程的 inheritableThreadLocals
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    }
}

由此可知,在创建新线程时,在初始化时其实相关逻辑是帮我们干了复制 inheritableThreadLocals 的操作,至此真相大白

总结

看完本文,相信大家对 Threadlocal 与 InheritableThreadLocal 的使用及其底层原理的掌握已不存在疑问,这也提醒我们熟练地掌握一个组件或一项技术最好的方式还是熟读它的源码,毕竟源码之下无秘密,当我们使用到别人封装好的组件或类时,如果有兴趣也可以也看一下它的源码,以本文为例,其实我们工程中多处地方都使用了 Context.getContext().getClientInfo();这样的获取客户端信息的形式,用惯了导致在多线程环境下没有引起警惕,以致踩了坑。

另外需要注意的是 ThreadLocal 使用不当可能导致内存泄漏,需要在线程结束后及时 remove 掉,这些技术细节不是本文重点,故而没有深入详解,有兴趣的大家可以去查阅相关资料。

 

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

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

相关文章

前端学习笔记:CSS的引入,元素选择器

这是本人学习的总结&#xff0c;主要学习资料如下 马士兵教育 目录 1、引入CSS1.1、引入CSS的方式1.2、优先级 2、元素选择器2.1、基本选择器2.1.1、选择器2.1.2、优先级 2.2、关系选择器2.2.1、优先级 2.3、属性选择器2.4、伪类选择器 1、引入CSS 1.1、引入CSS的方式 第一个…

互斥锁深度理解与使用

大家好&#xff0c;我是易安! 我们知道一个或者多个操作在CPU执行的过程中不被中断的特性&#xff0c;称为“原子性”。理解这个特性有助于你分析并发编程Bug出现的原因&#xff0c;例如利用它可以分析出long型变量在32位机器上读写可能出现的诡异Bug&#xff0c;明明已经把变量…

SpringCloud--gateway 网关

在Spring Cloud中&#xff0c;使用Gateway网关访问服务可以有多种好处&#xff0c;包括但不限于以下几点&#xff1a; 统一入口管理&#xff1a;Gateway作为统一的服务入口&#xff0c;可以对所有的请求进行统一管理和控制&#xff0c;实现微服务集中管理。 动态路由&#xff…

056:cesium 七种方法设置颜色

第056个 点击查看专栏目录 本示例的目的是介绍如何在vue+cesium中设置颜色,这里用到了7种方法,查看API,还有很多种方法 直接复制下面的 vue+cesium源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共115行)相关API参考:专栏目标示例效果 配置…

深入理解Go语言中的接口编程【17】

文章目录 接口接口接口类型为什么要使用接口接口的定义实现接口的条件接口类型变量值接收者和指针接收者实现接口的区别值接收者实现接口指针接收者实现接口下面的代码是一个比较好的面试题 类型与接口的关系一个类型实现多个接口多个类型实现同一接口接口嵌套 空接口空接口的定…

TCP教程:详解TCP连接过程

目录标题 一 、简述二 、TCP建立连接协议&#xff08;三次握手&#xff09;2.1 概述及目的2.2 第一次握手&#xff1a;客户端发送SYN报文2.3 第二次握手&#xff1a;服务器回应SYN-ACK报文2.4 第三次握手&#xff1a;客户端回应ACK报文2.5 顾客预定座位场景2.6底层原理2.7 TCP …

嵌入式之Samba服务器搭建

在嵌入式系统开发应用平台中&#xff0c;tftp、nfs和samba服务器是最常用的文件传输工具 tftp和nfs是在嵌入式Linux开发环境中经常使用的传输工具 samba则是Linux和Windows之间的文件传输工具。 下面演示在linux上搭建Samba服务器 sudo apt-get install samba chmod -R 77…

会场安排问题——算法实现(C实现)

问题描述&#xff1a;加入要在足够多的会场里安排一批活动&#xff0c;并希望使用尽可能少的会场。设计一个有效的贪心算法进行安排。&#xff08;这个问题实际上是著名的图着色问题。若每个活动作为图的一个顶点&#xff0c;不相容活动之间用边相连。使相连顶点着有不同颜色的…

数据库原理容易出错的点

一个数据库只存在一个内模式和一个模式&#xff0c;可以存在多个外模式除了删除表或视图的使用的是DELETE以外其他数据库对象均是使用DROP遵守两段封锁的协议的并发事务一定是可串行化的哪些情况下不适合创建索引&#xff1a; 对于查询过程中很少使用或参考的列对于那些只有很少…

【无人车】用于无人地面车辆的路径跟踪算法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

CMIP6数据处理:WRF模式动力降尺度、单点降尺度、统计方法区域降尺度、SWAT数据、Biome-BGC数据制备

查看原文>>>WRF模式、CMIP6数据处理、机器学习气象海洋水文应用、python地球科学 目录 CMIP6月数据、日数据、全球VIPPHEN物候数据、ERA5数据 一、CMIP6中的模式比较计划 二、数据下载 三、基础知识 四、单点降尺度 五、统计方法的区域降尺度 六、基于WRF模式…

助力低碳出行 | 基于ACM32 MCU的电动滑板车方案

前言 随着智能科技的快速发展&#xff0c;电动滑板车的驱动系统也得到了长足的发展。国内外的电动滑板车用电机驱动系统分为传统刷式电机和无刷电机两种类型。其中&#xff0c;传统的刷式电机已经逐渐被无刷电机所取代&#xff0c;无刷电机的性能和寿命都更出色&#xff0c;已…

STM32F4 HAL库使用DMA进行ADC采样实时发送波形到串口显示(包含傅里叶变换)

1.总体逻辑 按下STM32F4的KEY0按键&#xff0c;通过外部中断的方式对按键进行检测&#xff0c;然后开启一次带DMA的固定点数的ADC采集&#xff0c;采集完成后在DMA的中断发送采集到的数据&#xff0c;然后清空数据区准备下一次的按键中断。电脑接受到串口数据后对数据进行简单…

大厂齐出海:字节忙种草,网易爱社交

配图来自Canva可画 随着国内移动互联网红利逐渐触顶&#xff0c;互联网市场日趋饱和&#xff0c;国内各互联网企业之间的竞争便愈发激烈起来。在此背景下&#xff0c;广阔的海外市场就成为了腾讯、阿里、字节、京东、拼多多、百度、网易、快手、B站等互联网公司关注和争夺的重…

算法--前缀和技巧 (蓝桥杯123-灵能传输--求和)

文章目录 什么是前缀和用途什么时候用java的前缀和例题[蓝桥杯 2022 省 A] 求和题目描述思路代码 [蓝桥杯 2021 国 ABC] 123题目描述思路代码 [蓝桥杯 2019 省 B] 灵能传输(蓝桥杯96%&#xff0c;洛谷ac)题目描述思路代码 什么是前缀和 如果一个数组a的元素为 a 1 , a 2 , a 3…

Springboot +Flowable,详细解释啥叫流程实例(一)

一.简介 上一篇中学习了Flowable 中的流程模板&#xff08;流程定义&#xff09;的部署问题&#xff0c;这一篇来学习什么叫流程实例。 部署之后的流程模板&#xff0c;还不能直接运行&#xff0c;例如我们部署了一个请假流程&#xff0c;现在 张三想要请假&#xff0c;他就需…

WhatsApp多开攻略,低成本高效率多开账号聊单的方法献上~

WhatsApp多开攻略&#xff01;低成本高效率多开账号聊单的方法献上~ WhatsApp多开是指在同一台设备上同时登录多个WhatsApp账号&#xff0c;这种技术通常被跨境电商从业者用于在不同的WhatsApp账号之间切换&#xff0c;以便更好地管理跨境电商业务。 图中工具&#xff1a; ss客…

lazada按关键字搜索商品API接口

lazada按关键字搜索商品 API接口&#xff0c;在 lazada上搜索产品&#xff0c;如果只需要搜索单个产品的话&#xff0c;那么直接在搜索框输入“关键字”即可&#xff0c;如果需要多个产品&#xff0c;那么则需要进行关键字扩展。 lazada按关键字搜索商品 API接口分为两部分&am…

AI 这是要杀疯啦!

ChatGPT 是基于 GPT 系列大模型开发出来的一个对话场景的 Demo&#xff0c;它已经让我们见识到了大模型的威力。 但有些开发者的胃口不满足于此&#xff0c;已经开始尝试“突破” AI 的边界了&#xff0c;本文推荐 5 个人工智能的开源项目。其中前两个项目&#xff0c;让人细思…

《港联证券》股票必须持仓多久才能卖?股票买入多久显示持仓?

有的新手股民在炒股的时候&#xff0c;对股票的了解是不多的&#xff0c;就会在网上搜索材料来进行学习&#xff0c;那么股票有必要持仓多久才干卖&#xff1f;股票买入多久显现持仓&#xff1f;港联证券为我们预备了相关内容&#xff0c;以供参阅。 股票有必要持仓多久才干卖&…