多线程引发的惨案直接把年终给干没了
前些日子我们线上出现了一个比较严重的故障,这个故障是多线程使用不当引起的,挺有代表性的,所以分享给大家,希望能帮大家避坑问题简述
先简单介绍一下问题产生的背景,我们有个返利业务,其中有个搜索场景,这个场景是用户在app输入搜索关键词,然后server会根据这个关键词到各个平台(如淘宝,京东,拼多多等)调一下搜索接口,聚合这些搜索结果后再返回给用户,最开始这个搜索场景处理是单线程的,但随着接入的平台越来越多,搜索请求耗时也越来越长,由于每个平台的搜索请求都是独立的,很显然,单线程是可以优化为多线程的,如下
img
这样的话,搜索请求的耗时就只取决于搜索接口耗时最长的那个平台,所以使用多线程显然对接口性能是一个极大的优化,但使用多线程改造上线后,短时间内社群中有多名用户反馈前台展示APP需要升级的提示,经定位后发现是因为在多线程中无法获取客户端信息,由于客户端信息缺失,导致返回给用户需要升级的提示,伪代码如下开启多线程处理newThread(newRunnable(){Overridepublicvoidrun(){MapclientInfoMapContext。getContext()。getClientInfo();无法获取客户端信息,返回需要升级的信息if(clientInfoMapnull){thrownewException(版本号过低,请升级版本);}StringversionclientInfoMap。get(version);以下正常逻辑。。。。}})。start();
画外音:在生产中多线程使用的是线程池来实现,这里为了方便演示,直接newThread,效果都一样,大家知道即可
那么问题来了,改成多线程后客户端信息怎么就取不到了呢?要搞清楚这个问题,就得先了解客户端信息是如何存储的了Threadlocal简介
不同客户端请求的客户端信息(wifi还是4G,机型,app名称,电量等)显然不一样,dubbo业务线程拿到客户端请求后首先会将有用的请求信息提取出来(如本文中的MapclientInfo),但这个clientInfo可能会在线程调用的各个方法中用到,于是如何存储就成为了一个现实的问题,相信有经验的朋友一下就想到了,没错,用Threadlocal!为什么用它,它有什么优势,简单来说有两点无锁化提升并发性能简化变量的传递逻辑1。无锁化提升并发性能
先说第一个,无锁化提升并发性能,影响并发的原因有很多,其中一个很重要的原因就是锁,为了防止对共享变量的竞用,不得不对共享变量加锁
如果对共享变量争用的线程数增多,显然会严重影响系统的并发度,最好的办法就是使用影分身术为每个线程都创建一个线程本地变量,这样就避免了对共享变量的竞用,也就实现了无锁化
无锁化
ThreadLocal即线程本地变量,它可以为每个线程创建一份线程本地变量,使用方法如下staticThreadLocalSimpleDateFormatthreadLocal1newThreadLocalSimpleDateFormat(){OverrideprotectedSimpleDateFormatinitialValue(){returnnewSimpleDateFormat(yyyyMMdd);}};publicStringformatDate(Datedate){returnthreadLocal1。get()。format(date);}
这样的话每个线程就独享一份与其他线程无关的SimpleDateFormat实例副本,它们调用formatDate时使用的SimpleDateFormat实例也是自己独有的副本,无论对副本怎么操作对其他线程都互不影响
通过以上例子我们可以看出,可以通过newThreadLocalinitialValue来为创建的ThreadLocal实例初始化本地变量(initialValue方法会在首次调用get时被调用以初始化本地变量)。当然,如果之后需要修改本地变量的话,也可以用以下方式来修改threadLocal1。set(newSimpleDateFormat(yyyyMMdd))
而使用threadLocal1。get()这样的方法即可获得线程本地变量
可能一些朋友会好奇线程本地变量是如何存储的,一图胜千言
每一个线程(Thread)内部都有一个ThreadLocalMap,ThreadLocal的get和set操作其实在底层都是针对ThreadLocalMap进行操作的publicclassThreadimplementsRunnable{ThreadLocalvaluespertainingtothisthread。ThismapismaintainedbytheThreadLocalclass。ThreadLocal。ThreadLocalMapthreadL}
它与HashMap类似,存储的都是键值对,只不过每一项(Entry)中的key为threadlocal变量(如上文案例中的threadLocal1),value才为我们要存储的值(如上文中的SimpleDateFormat实例),此外它们在碰到hash冲突时的处理策略也不同,HashMap在碰到hash冲突时采用的是链表法,而ThreadLocalMap采用的是线性探测法2。简化变量的传递逻辑
接下来我们来看使用ThreadLocal的等二个好处,简化变量的传递逻辑,线程在处理业务逻辑时可能会调用几十个方法,如果这些方法中只有几个需要用到clientInfo,难道要在这几十个方法中定义一个clientInfo参数来层层传递吗,显然不现实。那该怎么办呢,使用ThreadLocal即可解决此问题。由上文可知通过ThreadLocal设置的本地变量是同threadlocal一起保存在Thread的ThreadLocalMap这个内部类中的,所以可在线程调用的任意方法中取出,伪代码如下publicclassThreadLocalWithUserContextimplementsRunnable{privatestaticThreadLocalMapString,StringthreadLocalnewThreadLocal();Overridepublicvoidrun(){clientInfo初始化MapString,StringclientIthreadLocal。set(clientInfo);test1();}publicvoidtest1(){test2();}publicvoidtest2(){testX();}。。。publicvoidtestX(){MapclientInfothreadLocal。get();}}
中间定义的任何方法都无需为了传递clientInfo而定义一个额外的变量,代码优雅了不少
由以上分析可知,使用ThreadLocal确实比较方便,在此我们先停下来思考一个问题:如果线程在调用过程中只用到一个clientInfo这样的信息,只定义一个ThreadLocal变量当然就够了,但实际上在使用过程中我们可能要传递多个类似clientInfo这样的信息(如userId,cookie,header),难道因此要定义多个ThreadLocal变量吗?
这么做不是不可以,但不够优雅,更合适的做法是我们只定义一个ThreadLocal变量,变量存的是一个上下文对象,其他像clientInfo,userId,header等信息就作为此上下文对象的属性即可,代码如下publicfinalclassContext{privatestaticfinalThreadLocalContextLOCALnewThreadLocalContext(){protectedContextinitialValue(){returnnewContext();}};privateL用户uidprivateMapString,StringclientI客户端信息privateMapString,S请求头信息privateMapString,MapString,S请求cookiepublicstaticContextgetContext(){return(Context)LOCAL。get();}}
这样的话我们可通过Context。getContext()。getXXX()的形式来获取线程所需的信息,通过这样的方式我们不仅避免了定义无数ThreadLocal变量的烦恼,而且还收拢了上下文信息的管理
通过以上介绍相信大家也都知道了clientInfo其实是借由ThreadLocal存储的,认清了这个事实后那我们现在再回头看开头的生产问题:将单线程改成多线程后,为什么在新线程中就拿不到clientInfo了?问题剖析
源码之下无秘密,我们查看一下源码来一探究竟,获取本地变量的值使用的是ThreadLocal。get方法,那就来看下这个方法publicclassThreadLocalT{publicTget(){1。先获取当前线程ThreadtThread。currentThread();2。再获取当前线程的ThreadLocalMapThreadLocalMapmapgetMap(t);if(map!null){ThreadLocalMap。Entryemap。getEntry(this);if(e!null){Tresult(T)e。}}returnsetInitialValue();}}
可以看到get方法主要步骤如下首先需要获取当前线程其次获取当前线程的ThreadLocalMap进而再去获取相应的本地变量值如果没有的话则调用initiaValue方法来初始化本地变量
由此可知当我们调用threadlocal。get时,会拿到当前线程的ThreadLocalMap,然后再去拿entry中的本地变量,而对多线程来说,新线程的ThreadLocalMap里面的东西本来就未做任何设置,是空的,拿不到线程本地变量也就合情合理了解决方案
问题清楚了,那怎么解决呢,不难得知主要有两种方案
1。我们之前是在新线程的执行方法中调用threadlocal。get方法,可以改成先从当前执行线程中调用threadlocal。get获得clientInfo,然后再把clientInfo传入新线程,伪代码如下先从当前线程的Context中获取clientInfoMapclientInfoMapContext。getContext()。getClientInfo();newThread(newRunnable(){Overridepublicvoidrun(){此时的clientInfoMap由于是在新线程创建前获取的,肯定是有值的StringversionclientInfoMap。get(version);以下正常逻辑。。。。}})。start();
2。只需把ThreadLocal换成InheritableThreadLocal,如下publicfinalclassContext{privatestaticfinalInheritableThreadLocalContextLOCALnewInheritableThreadLocalContext(){protectedContextinitialValue(){returnnewContext();}};publicstaticContextgetContext(){return(Context)LOCAL。get();}}newThread(newRunnable(){Overridepublicvoidrun(){此时的clientInfo能正常获取到MapclientInfoContext。getContext()。getClientInfo();StringversionclientInfo。get(version);以下正常逻辑。。。。}})。start();
为什么InheritableThreadLocal能有这么神奇,背后的原理是什么?
由前文介绍我们得知,ThreadLocal变量最终是存在ThreadLocalMap中的,那么能否在创建新线程的时候,把当前线程的ThreadLocalMap复制给新线程的ThreadLocalMap呢,这样的话即便你从新线程中调用threadlocal。get也照样能获得对应的本地变量,和InheritableThreadLocal相关的底层干的就是这个事,我们先来瞧一瞧InheritableThreadLocal长啥样publicclassInheritableThreadLocalTextendsThreadLocalT{ThreadLocalMapgetMap(Threadt){returnt。inheritableThreadL}voidcreateMap(Threadt,TfirstValue){t。inheritableThreadLocalsnewThreadLocalMap(this,firstValue);}}
由此可知InheritableThreadLocal其实是继承自ThreadLocal类的,此外我们在getMap和createMap这两个方法中也发现它的底层其实是用inheritableThreadLocals来存储的,而ThreadLocal用的是threadLocals变量存储的publicclassThreadimplementsRunnable{ThreadLocal实例的底层存储ThreadLocal。ThreadLocalMapthreadLinheritableThreadLocals实例的底层存储ThreadLocal。ThreadLocalMapinheritableThreadL}
知道了这些,我们再来看下创建线程时涉及到的inheritableThreadLocals复制相关的关键代码如下:publicclassThreadimplementsRunnable{publicThread(){init(null,null,ThreadnextThreadNum(),0);}privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize){init(g,target,name,stackSize,null,true);}privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize,AccessControlContextacc,booleaninheritThreadLocals){。。。ThreadparentcurrentThread();if(inheritThreadLocalsparent。inheritableThreadLocals!null)将当前线程的inheritableThreadLocals复制给新创建线程的inheritableThreadLocalsthis。inheritableThreadLocalsThreadLocal。createInheritedMap(parent。inheritableThreadLocals);}}
由此可知,在创建新线程时,在初始化时其实相关逻辑是帮我们干了复制inheritableThreadLocals的操作,至此真相大白总结
看完本文,相信大家对Threadlocal与InheritableThreadLocal的使用及其底层原理的掌握已不存在疑问,这也提醒我们熟练地掌握一个组件或一项技术最好的方式还是熟读它的源码,毕竟源码之下无秘密,当我们使用到别人封装好的组件或类时,如果有兴趣也可以也看一下它的源码,以本文为例,其实我们工程中多处地方都使用了Context。getContext()。getClientInfo();这样的获取客户端信息的形式,用惯了导致在多线程环境下没有引起警惕,以致踩了坑。
另外需要注意的是ThreadLocal使用不当可能导致内存泄漏,需要在线程结束后及时remove掉,这些技术细节不是本文重点,故而没有深入详解,有兴趣的大家可以去查阅相关资料来源:https:mp。weixin。qq。comsObXr7SR9TJC6aqTIOYlqA
作者:坤哥