0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

多线程引发的惨案!

小林coding 来源:小林coding 2023-02-07 15:30 次阅读

今天分享一位朋友线上出现了一个比较严重的故障,这个故障是多线程使用不当引起的。

挺有代表性的,所以分享给大家,希望能帮大家避坑。

问题简述

先简单介绍一下问题产生的背景,我们有个返利业务。

其中有个搜索场景,这个场景是用户在 app 输入搜索关键词,然后 server 会根据这个关键词到各个平台(如淘宝,京东,拼多多等)调一下搜索接口聚合这些搜索结果后再返回给用户。

最开始这个搜索场景处理是单线程的,但随着接入的平台越来越多,搜索请求耗时也越来越长,由于每个平台的搜索请求都是独立的,很显然,单线程是可以优化为多线程的,如下

97f0512e-a44b-11ed-bfe3-dac502259ad0.pngimg

这样的话,搜索请求的耗时就只取决于搜索接口耗时最长的那个平台。

所以使用多线程显然对接口性能是一个极大的优化,但使用多线程改造上线后,短时间内社群中有多名用户反馈前台展示「APP 需要升级的提示」。

经定位后发现是因为在多线程中无法获取客户端信息,由于客户端信息缺失,导致返回给用户需要升级的提示,伪代码如下:

//开启多线程处理
newThread(newRunnable(){
@Override
publicvoidrun(){
MapclientInfoMap=Context.getContext().getClientInfo();
//无法获取客户端信息,返回需要升级的信息
if(clientInfoMap==null){
thrownewException("版本号过低,请升级版本");
}
Stringversion=clientInfoMap.get("version");


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

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

那么问题来了,改成多线程后客户端信息怎么就取不到了呢?

要搞清楚这个问题,就得先了解客户端信息是如何存储的了。

Threadlocal 简介

不同客户端请求的客户端信息(wifi 还是 4G,机型,app名称,电量等)显然不一样,dubbo 业务线程拿到客户端请求后首先会将有用的请求信息提取出来(如本文中的 MapclientInfo)。

但这个 clientInfo 可能会在线程调用的各个方法中用到,于是如何存储就成为了一个现实的问题。

相信有经验的朋友一下就想到了,没错,用 Threadlocal

为什么用它,它有什么优势,简单来说有两点

  1. 无锁化提升并发性能

  2. 简化变量的传递逻辑

1.无锁化提升并发性能

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

980c0f36-a44b-11ed-bfe3-dac502259ad0.png

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

981ee5ac-a44b-11ed-bfe3-dac502259ad0.png无锁化

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

staticThreadLocalthreadLocal1=newThreadLocal(){
@Override
protectedSimpleDateFormatinitialValue(){
returnnewSimpleDateFormat("yyyy-MM-dd");
}
};

publicStringformatDate(Datedate){
returnthreadLocal1.get().format(date);
}

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

通过以上例子我们可以看出,可以通过 new ThreadLocal+ initialValue 来为创建的 ThreadLocal 实例初始化本地变量(initialValue 方法会在首次调用 get 时被调用以初始化本地变量)。

当然,如果之后需要修改本地变量的话,也可以用以下方式来修改

threadLocal1.set(newSimpleDateFormat("yyyy-MM-dd"))

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

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

982d93b8-a44b-11ed-bfe3-dac502259ad0.png

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

publicclassThreadimplementsRunnable{
/*ThreadLocalvaluespertainingtothisthread.Thismapismaintained
*bytheThreadLocalclass.*/
ThreadLocal.ThreadLocalMapthreadLocals=null;
}

它与 HashMap 类似,存储的都是键值对,只不过每一项(Entry)中的 key 为 threadlocal 变量(如上文案例中的 threadLocal1),value 才为我们要存储的值(如上文中的 SimpleDateFormat 实例)。

此外它们在碰到 hash 冲突时的处理策略也不同,HashMap 在碰到 hash 冲突时采用的是链表法,而 ThreadLocalMap 采用的是线性探测法

2.简化变量的传递逻辑

接下来我们来看使用 ThreadLocal 的等二个好处,简化变量的传递逻辑。

线程在处理业务逻辑时可能会调用几十个方法,如果这些方法中只有几个需要用到 clientInfo,难道要在这几十个方法中定义一个 clientInfo 参数来层层传递吗,显然不现实。

那该怎么办呢,使用 ThreadLocal 即可解决此问题。

由上文可知通过 ThreadLocal 设置的本地变量是同 threadlocal 一起保存在 Thread 的 ThreadLocalMap 这个内部类中的,所以可在线程调用的任意方法中取出,伪代码如下:

publicclassThreadLocalWithUserContextimplementsRunnable{

privatestaticThreadLocal>threadLocal
=newThreadLocal<>();

@Override
publicvoidrun(){
//clientInfo初始化
MapclientInfo=xxx;
threadLocal.set(clientInfo);
test1();
}

publicvoidtest1(){
test2();
}

publicvoidtest2(){
testX();
}
...

publicvoidtestX(){
MapclientInfo=threadLocal.get();
}
}

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

由以上分析可知,使用 ThreadLocal 确实比较方便。

在此我们先停下来思考一个问题:如果线程在调用过程中只用到一个 clientInfo 这样的信息,只定义一个 ThreadLocal 变量当然就够了,但实际上在使用过程中我们可能要传递多个类似 clientInfo 这样的信息(如 userId,cookie,header),难道因此要定义多个 ThreadLocal 变量吗?

这么做不是不可以,但不够优雅。

更合适的做法是我们只定义一个 ThreadLocal 变量,变量存的是一个上下文对象,其他像 clientInfo,userId,header 等信息就作为此上下文对象的属性即可,代码如下:

publicfinalclassContext{

privatestaticfinalThreadLocalLOCAL=newThreadLocal(){
protectedContextinitialValue(){
returnnewContext();
}
};


privateLonguid;//用户uid
privateMapclientInfo;//客户端信息
privateMapheaders=null;//请求头信息
privateMap>cookies=null;//请求cookie

publicstaticContextgetContext(){
return(Context)LOCAL.get();
}

}

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

通过以上介绍相信大家也都知道了 clientInfo 其实是借由 ThreadLocal 存储的。

认清了这个事实后那我们现在再回头看开头的生产问题:将单线程改成多线程后,为什么在新线程中就拿不到 clientInfo 了?

问题剖析

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

publicclassThreadLocal<T>{
publicTget(){
//1.先获取当前线程
Threadt=Thread.currentThread();
//2.再获取当前线程的ThreadLocalMap
ThreadLocalMapmap=getMap(t);
if(map!=null){
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
Tresult=(T)e.value;
returnresult;
}
}
returnsetInitialValue();
}
}

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

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

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

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

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

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

解决方案

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

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

//先从当前线程的Context中获取clientInfo
MapclientInfoMap=Context.getContext().getClientInfo();
newThread(newRunnable(){
@Override
publicvoidrun(){
//此时的clientInfoMap由于是在新线程创建前获取的,肯定是有值的
Stringversion=clientInfoMap.get("version");


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

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

publicfinalclassContext{
privatestaticfinalInheritableThreadLocalLOCAL=newInheritableThreadLocal(){
protectedContextinitialValue(){
returnnewContext();
}
};

publicstaticContextgetContext(){
return(Context)LOCAL.get();
}
}

newThread(newRunnable(){
@Override
publicvoidrun(){
//此时的clientInfo能正常获取到
MapclientInfo=Context.getContext().getClientInfo();
Stringversion=clientInfo.get("version");
//以下正常逻辑
....
}
}).start();

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

由前文介绍我们得知,ThreadLocal 变量最终是存在 ThreadLocalMap 中的。

那么能否在创建新线程的时候,把当前线程的 ThreadLocalMap 复制给新线程的 ThreadLocalMap 呢?

这样的话即便你从新线程中调用 threadlocal.get 也照样能获得对应的本地变量,和 InheritableThreadLocal 相关的底层干的就是这个事。

我们先来瞧一瞧 InheritableThreadLocal 长啥样:

publicclassInheritableThreadLocal<T>extendsThreadLocal<T>{

ThreadLocalMapgetMap(Threadt){
returnt.inheritableThreadLocals;
}

voidcreateMap(Threadt,TfirstValue){
t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);
}
}

由此可知 InheritableThreadLocal 其实是继承自 ThreadLocal 类的。

此外我们在 getMap 和 createMap 这两个方法中也发现它的底层其实是用 inheritableThreadLocals 来存储的,而 ThreadLocal 用的是 threadLocals 变量存储的。

publicclassThreadimplementsRunnable{
//ThreadLocal实例的底层存储
ThreadLocal.ThreadLocalMapthreadLocals=null;

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

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

public
classThreadimplementsRunnable{
publicThread(){
init(null,null,"Thread-"+nextThreadNum(),0);
}

privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,
longstackSize){
init(g,target,name,stackSize,null,true);
}

privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,
longstackSize,AccessControlContextacc,
booleaninheritThreadLocals){
...
Threadparent=currentThread();
if(inheritThreadLocals&&parent.inheritableThreadLocals!=null)
//将当前线程的inheritableThreadLocals复制给新创建线程的inheritableThreadLocals
this.inheritableThreadLocals=
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
}

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

总结

看完本文,相信大家对 Threadlocal 与 InheritableThreadLocal 的使用及其底层原理的掌握已不存在疑问。

这也提醒我们熟练地掌握一个组件或一项技术最好的方式还是熟读它的源码,毕竟源码之下无秘密。

当我们使用到别人封装好的组件或类时,如果有兴趣也可以也看一下它的源码。

以本文为例,其实我们工程中多处地方都使用了 Context.getContext().getClientInfo();这样的获取客户端信息的形式,用惯了导致在多线程环境下没有引起警惕,以致踩了坑。

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

历史好文:

多个线程为了同个资源打起架来了,该如何让他们安分?

美团三面:一直追问我, MySQL 幻读被彻底解决了吗?

原来墙,是这么把我 TCP 连接干掉的!

面试官:你确定 Redis 是单线程的进程吗?

字节一面:HTTPS 一定安全可靠吗?


审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 多线程
    +关注

    关注

    0

    文章

    277

    浏览量

    19886
  • 代码
    +关注

    关注

    30

    文章

    4708

    浏览量

    68173
  • 变量
    +关注

    关注

    0

    文章

    613

    浏览量

    28298

原文标题:多线程引发的惨案!直接把年终给干没了

文章出处:【微信号:小林coding,微信公众号:小林coding】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Java多线程的用法

    本文将介绍一下Java多线程的用法。 基础介绍 什么是多线程 指的是在一个进程中同时运行多个线程,每个线程都可以独立执行不同的任务或操作。 与单线程
    的头像 发表于 09-30 17:07 907次阅读

    多线程技术在串口通信中的应用

            首先介绍了多线程技术的基本原理,然后讨论了多线程技术在串口通信中的应用,并给出了实现的方法和步骤。关键词:多线程;串口通信;事件
    发表于 09-04 09:10 18次下载

    多线程与聊天室程序的创建

    多线程程序的编写,多线程应用中容易出现的问题。互斥对象的讲解,如何采用互斥对象来实现多线程的同步。如何利用命名互斥对象保证应用程序只有一个实例运行。应用多线程编写网络聊天室程序。
    发表于 05-16 15:22 0次下载

    设计多线程和多核系统

    如果您的微控制器应用程序需要处理数字音频,请考虑采用多线程方法。使用多线程设计方法可以使设计者以简单的方式重用其部分设计。
    发表于 08-14 15:42 9次下载
    设计<b class='flag-5'>多线程</b>和多核系统

    linux多线程编程技术

    1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的 Unix也支持线程的概念,但是在一个进程
    发表于 10-24 16:01 5次下载

    多线程好还是单线程好?单线程多线程的区别 优缺点分析

    摘要:如今单线程多线程已经得到普遍运用,那么到底多线程好还是单线程好呢?单线程多线程的区别又
    发表于 12-08 09:33 8.1w次阅读

    mfc多线程编程实例及代码,mfc多线程间通信介绍

    摘要:本文主要以MFC多线程为中心,分别对MFC多线程的实例、MFC多线程之间的通信展开的一系列研究,下面我们来看看原文。
    发表于 12-08 15:23 1.8w次阅读
    mfc<b class='flag-5'>多线程</b>编程实例及代码,mfc<b class='flag-5'>多线程</b>间通信介绍

    什么是多线程编程?多线程编程基础知识

    摘要:多线程编程是现代软件技术中很重要的一个环节。要弄懂多线程,这就要牵涉到多进程。本文主要以多线程编程以及多线程编程相关知识而做出的一些结论。
    发表于 12-08 16:30 1.2w次阅读

    SpringBoot实现多线程

    SpringBoot实现多线程
    的头像 发表于 01-12 16:59 1773次阅读
    SpringBoot实现<b class='flag-5'>多线程</b>

    labview AMC多线程

    labview_AMC多线程
    发表于 08-21 10:31 27次下载

    多线程idm下载软件

    多线程idm下载软件
    发表于 10-23 09:23 0次下载

    多线程如何保证数据的同步

    多线程编程是一种并发编程的方法,意味着程序中同时运行多个线程,每个线程可独立执行不同的任务,共享同一份数据。由于多线程并发执行的特点,会引发
    的头像 发表于 11-17 14:22 1075次阅读

    mfc多线程编程实例

    (图形用户界面)应用程序的开发。在这篇文章中,我们将重点介绍MFC中的多线程编程。 多线程编程在软件开发中非常重要,它可以实现程序的并发执行,提高程序的效率和响应速度。MFC提供了丰富的多线程支持,可以轻松地实现
    的头像 发表于 12-01 14:29 1351次阅读

    redis多线程还能保证线程安全吗

    Redis是一种使用C语言编写的高性能键值存储系统,它是单线程的,因为使用了多路复用的方式来处理并发请求。这样的实现方式带来了很好的性能,但同时也引发了一些线程安全方面的问题。 在Redis中,由于
    的头像 发表于 12-05 10:28 1579次阅读

    java实现多线程的几种方式

    Java实现多线程的几种方式 多线程是指程序中包含了两个或以上的线程,每个线程都可以并行执行不同的任务或操作。Java中的多线程可以提高程序
    的头像 发表于 03-14 16:55 532次阅读