当前位置: 首页 > 科技观察

多线程造成的悲剧直接抹杀了年底

时间:2023-03-18 12:48:28 科技观察

大家好,我是鲲哥。几天前,我们的线路发生了严重故障。这个故障是多线程使用不当造成的。很有代表性。对,所以分享给大家,希望能帮助大家避坑。问题简介首先简单介绍一下问题的背景。我们有返利业务,有搜索场景。这个关键词被转移到各个平台(如淘宝、京东、拼多多等)调整搜索界面,聚合这些搜索结果返回给用户。一开始这个搜索场景的处理是单线程的,但是随着接入平台越来越多,搜索请求耗时越来越长。由于各个平台的搜索请求都是独立的,显然单线程可以优化成多线程。在下面的案例中,搜索请求的耗时仅取决于搜索接口耗时最长的平台,所以使用多线程显然是对接口性能的极大优化,但是使用多线程之后改造上线,社区很多用户反映前台显示“APP需要升级提示”,定位后发现多线程无法获取客户端信息,由于缺少客户端信息,返回给用户需要升级的提示。伪代码如下//启用多线程处理newThread(newRunnable(){@Overridepublicvoidrun(){MapclientInfoMap=Context.getContext().getClientInfo();//无法获取客户端信息,返回需要升级的信息if(clientInfoMap==null){thrownewException("版本号太低,请升级版本");}Stringversion=clientInfoMap.get("version");//遵循正常逻辑....}}).start();画外音:生产中多线程使用线程池来实现。为了演示方便,直接newThread也是一样的效果,大家都懂的。那么问题来了,为什么改成多线程后获取不到客户端信息呢?要理解这个问题,首先要理解客户端信息是如何存储的。Threadlocal引入不同客户端请求的客户端信息(wifi或4G、型号、应用名称、电池等))明显不同。dubbo业务线程拿到client请求后,会先提取有用的请求信息(比如本文中的MapclientInfo),但是这个clientInfo可能会在线程调用的各种方法中用到,所以如何存储取决于它有成为真正的问题,相信有经验的朋友马上就会想到,没错,用Threadlocal!为什么使用它,它有什么优势?简单来说有两点:无锁提升并发性能简化变量的传递逻辑1.无锁提升并发性能先说第一点。无锁对并发性能的提升影响并发的原因有很多,其中最重要的一个原因就是锁。为了防止对共享变量的竞争,共享变量必须被锁定。如果竞争共享变量的线程数增多,显然会严重影响系统的并发性。最好的办法是使用“ShadowClone”为每个线程创建一个线程局部变量,这样就避免了对共享变量的竞争,实现了无锁无锁。ThreadLocal是一个线程局部变量,可以用来为每个线程创建一个线程局部变量,使用方法如下返回新的SimpleDateFormat("yyyy-MM-dd");}};publicStringformatDate(Datedate){returnthreadLocal1.get().format(date);}这样,每个线程都有一个与其他线程无关的SimpleDateFormat实例的独占副本,它们使用调用formatDate时的SimpleDateFormat实例也是自己唯一的副本,无论你如何操作副本,都不会影响其他线程。从上面的例子我们可以看出,可以使用newThreadLocal+initialValue为创建的ThreadLocal实例初始化局部变量(initialValue方法会在第一个get调用时调用,初始化局部变量)当然,如果后面需要修改局部变量,也可以通过如下方式修改threadLocal1.set(newSimpleDateFormat("yyyy-MM-dd")),使用threadLocal1.get()获取线程局部变量。有的朋友会很好奇线程局部变量是怎么存储的。一张图片胜过千言万语。每个线程(Thread)里面都有一个ThreadLocalMap。ThreadLocal的get和set操作实际上是在最底层的ThreadLocalMap上操作的。publicclassThreadimplementsRunnable{/*ThreadLocal值属于这个线程。该映射由ThreadLocal类维护*。*/ThreadLocal.ThreadLocalMapthreadLocals=null;}类似于HashMap,存储键值对,只是每一项(Entry)中的key是threadlocal变量(如上例中的threadLocal1),值为我们要存储的值(例如上面的SimpleDateFormat实例)。此外,它们在遇到哈希冲突时有不同的处理策略。HashMap遇到hash链表方法是用冲突的,而ThreadLocalMap使用的是线性检测方法2.简化变量的传递逻辑接下来我们看看使用ThreadLocal的两个好处,它简化了变量的传递逻辑。线程在处理业务逻辑的时候,可能会调用几十个方法,如果这些方法中只有少数几个需要用到clientInfo,那么在这几十个方法中定义一个clientInfo参数逐层传递显然不现实吗?那怎么办呢,用ThreadLocal来解决这个问题。从上面可以看出,ThreadLocal设置的局部变量是和threadlocal一起存放在Thread的ThreadLocalMap内部类中的,所以可以从线程调用的任何方法中获取。伪代码如下:publicclassThreadLocalWithUserContextimplementsRunnable{privatestaticThreadLocal>threadLocal=newThreadLocal<>();@Overridepublicvoidrun(){//clientInfo初始化MapclientInfo=xxx;threadLocal.set(clientInfo);测试1();}publicvoidtest1(){test2();}publicvoidtest2(){testX();}...publicvoidtestX(){MapclientInfo=threadLocal.get();}}中间定义的任何方法都不需要通过ClientInfo定义额外的变量,代码也优雅很多。从上面的分析可以看出使用ThreadLocal更方便。这里我们停下来思考一个问题:如果一个线程在调用过程中只使用clientInfo等信息,那么只定义一个ThreadLocal变量肯定就够了,但实际上我们可能需要传递多个clientInfo等信息(如userId,cookie,header)在使用过程中。是否需要定义多个ThreadLocal变量?是的,但不够优雅。比较合适的是我们只定义一个ThreadLocal变量,里面存放了一个context对象,其他的比如clientInfo,使用rId、header等信息可以作为这个context对象的属性,代码如下返回新上下文();}};私人长uid;//用户uidprivateMapclientInfo;//客户端信息privateMapheaders=null;//请求头信息privateMap>cookies=null;//请求cookiepublicstaticContextgetContext(){return(Context)LOCAL.get();}}这样,我们就可以通过Context.getContext().getXXX()的形式获取到线程所需的信息,这样既避免了定义无数ThreadLocal变量的麻烦,又收集了上下文信息的管理.相信大家通过上面的介绍都知道clientInfo其实是由ThreadLocal存储的。事后我们再回头看看最开始的生产问题:单线程改成多线程后,为什么在新的线程中获取不到clientInfo?问题分析源码下没有秘密。让我们检查源代码以找出答案。ThreadLocal.get方法是用来获取局部变量的值的,我们先来看看这个方法publicclassThreadLocal{publicTget(){//1.首先获取当前线程Threadt=Thread.currentThread();//2.再次获取当前线程的ThreadLocalMapThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){T结果=(T)e.value;返回结果;}}返回setInitialValue();}}可以看到get方法的主要步骤如下:首先需要获取当前线程,其次获取当前线程的ThreadLocalMap,然后获取对应的local如果没有变量值,调用initiaValue方法初始化局部变量。可以看出,当我们调用threadlocal.get时,会获取到当前线程的ThreadLocalMap,进而获取到入口中的局部变量。线程的ThreadLocalMap里面的东西没有任何设置,都是空的。不获取线程局部变量是合理的。就是在新线程的执行方法中调用threadlocal.get方法。可以改为从当前执行线程调用threadlocal.get获取clientInfo,然后将clientInfo传入新线程。伪代码如下//首先从当前线程的Context中获取clientInfoMapclientInfoMap=Context.getContext().getClientInfo();newThread(newRunnable(){@Overridepublicvoidrun(){//clientInfoMap此时必须在新线程创建前获取String版本的value=clientInfoMap.get("version");//下面正常逻辑....}}).start();2.将ThreadLocal换成InheritableThreadLocal即可,如下publicfinalclassContext{privatestaticfinalInheritableThreadLocalLOCAL=newInheritableThreadLocal(){protectedContextinitialValue(){returnnewContext();}};publicstaticContextgetContext(){return(Context)LOCAL.get();}}newThread(newRunnable(){@Overridepublicvoidrun(){//此时可以正常获取clientInfoMapclientInfo=Context.getContext().getClientInfo();Stringversion=clientInfo.get("version");//以下正常逻辑....}}).start();InheritableThreadLocal为什么这么神奇,背后的原理是什么?从前面的介绍我们知道,ThreadLocal变量最终存储在ThreadLocalMap中,那么当前线程的ThreadLocalMap也可以在创建新线程的时候被创建?复制到新线程的ThreadLocalMap呢,这样即使我如果你从新线程调用threadlocal.get,你仍然可以得到相应的局部变量。InheritableThreadLocal相关的底层就是干这个的。让我们看看InheritableThreadLocal是什么样子的publicclassInheritableThreadLocalextendsThreadLocal{ThreadLocalMapgetMap(Threadt){returnt.inheritableThreadLocals;}voidcreateMap(Threadt,TfirstValue){t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);}}由此可见,InheritableThreadLocal其实是继承自ThreadLocal类。另外,我们还发现它的底层其实是存储在getMap和createMap这两个方法中的inheritableThreadLocals中,而ThreadLocal使用的是publicclassThreadimplementsRunnable{//ThreadLocal实例的底层存储ThreadLocal.ThreadLocalMapthreadLocals=null;//inheritableThreadLocals实例的底层存储ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;}了解了这些,我们再来看创建线程时涉及的inheritableThreadLocals复制相关的关键代码如下:publicclassThreadimplementsRunnable{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到新建线程的inheritableThreadLocalsthis.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);}}由此可见,在创建新线程时,初始化时的相关逻辑是帮我们做复制inheritableThreadLocals的操作,至此真相大白。一个好方法是熟悉它的源代码。毕竟源代码下没有秘密。我们在使用别人封装的组件或者类的时候,有兴趣的也可以看看它的源码。以这篇文章为例。其实我们的项目Context.getContext().getClientInfo()在很多地方都用到了;这种获取客户端信息的形式,在多线程环境下,引起大家不警惕,让你踩坑另外需要注意的是,ThreadLocal使用不当可能会导致内存泄漏,需要在线程结束后及时清除。这些技术细节不是本文的重点,因此不做深入讲解。有兴趣的可以参考相关资料。