这个线上事故,直接把年终奖给干没了。。
大家好,我是鱼皮。
前些日子,一位朋友负责的产品线上出现了一个比较严重的故障,经过排查发现这个故障是多线程使用不当引起的。这个问题挺有代表性的,所以分享给大家,希望能帮大家避坑。
问题简述
先简单介绍一下问题产生的背景,有个返利业务,其中有个搜索场景,这个场景是用户在 app 输入搜索关键词,然后 server 会根据这个关键词到各个平台(如淘宝,京东,拼多多等)调一下搜索接口,聚合这些搜索结果后再返回给用户,最开始这个搜索场景处理是单线程的,但随着接入的平台越来越多,搜索请求耗时也越来越长,由于每个平台的搜索请求都是独立的,很显然,单线程是可以优化为多线程的,如下
(资料图)
这样的话,搜索请求的耗时就只取决于搜索接口耗时最长的那个平台,所以使用多线程显然对接口性能是一个极大的优化,但使用多线程改造上线后,短时间内社群中有多名用户反馈前台展示「APP 需要升级的提示」,经定位后发现是因为在多线程中无法获取客户端信息,由于客户端信息缺失,导致返回给用户需要升级的提示,伪代码如下
//开启多线程处理newThread(newRunnable(){@Overridepublicvoidrun(){MapclientInfoMap=Context.getContext().getClientInfo();//无法获取客户端信息,返回需要升级的信息if(clientInfoMap==null){thrownewException(\"版本号过低,请升级版本\");}Stringversion=clientInfoMap.get(\"version\");//以下正常逻辑....}}).start();
画外音:在生产中多线程使用的是线程池来实现,这里为了方便演示,直接 new Thread,效果都一样,大家知道即可
那么问题来了,改成多线程后客户端信息怎么就取不到了呢?
要搞清楚这个问题,就得先了解客户端信息是如何存储的了
Threadlocal 简介
不同客户端请求的客户端信息(wifi 还是 4G,机型,app名称,电量等)显然不一样,dubbo 业务线程拿到客户端请求后首先会将有用的请求信息提取出来(如本文中的 Map
无锁化提升并发性能
简化变量的传递逻辑
1.无锁化提升并发性能
先说第一个,无锁化提升并发性能,影响并发的原因有很多,其中一个很重要的原因就是锁,为了防止对共享变量的竞用,不得不对共享变量加锁
如果对共享变量争用的线程数增多,显然会严重影响系统的并发度,最好的办法就是使用“影分身术”为每个线程都创建一个线程本地变量,这样就避免了对共享变量的竞用,也就实现了无锁化
ThreadLocal即线程本地变量,它可以为每个线程创建一份线程本地变量,使用方法如下
staticThreadLocalthreadLocal1=newThreadLocal(){@OverrideprotectedSimpleDateFormatinitialValue(){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()
这样的方法即可获得线程本地变量
可能一些朋友会好奇线程本地变量是如何存储的,一图胜千言:
每一个线程(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
中间定义的任何方法都无需为了传递 clientInfo 而定义一个额外的变量,代码优雅了不少。
由以上分析可知,使用 ThreadLocal 确实比较方便,在此我们先停下来思考一个问题:如果线程在调用过程中只用到一个 clientInfo 这样的信息,只定义一个 ThreadLocal 变量当然就够了,但实际上在使用过程中我们可能要传递多个类似 clientInfo 这样的信息(如 userId,cookie,header),难道因此要定义多个 ThreadLocal 变量吗,这么做不是不可以,但不够优雅,更合适的做法是我们只定义一个 ThreadLocal 变量,变量存的是一个上下文对象,其他像 clientInfo,userId,header 等信息就作为此上下文对象的属性即可,代码如下
publicfinalclassContext{privatestaticfinalThreadLocalLOCAL=newThreadLocal(){protectedContextinitialValue(){returnnewContext();}};privateLonguid;//用户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){Tresult=(T)e.value;returnresult;}}returnsetInitialValue();}}
可以看到 get 方法主要步骤如下
首先需要获取当前线程
其次获取当前线程的 ThreadLocalMap
进而再去获取相应的本地变量值
如果没有的话则调用 initiaValue 方法来初始化本地变量
由此可知当我们调用 threadlocal.get 时,会拿到当前线程的 ThreadLocalMap,然后再去拿 entry 中的本地变量,而对多线程来说,新线程的 ThreadLocalMap 里面的东西本来就未做任何设置,是空的,拿不到线程本地变量也就合情合理了
解决方案
问题清楚了,那怎么解决呢,不难得知主要有两种方案
1.我们之前是在新线程的执行方法中调用 threadlocal.get 方法,可以改成先从当前执行线程中调用 threadlocal.get 获得 clientInfo,然后再把 clientInfo 传入新线程,伪代码如下
//先从当前线程的Context中获取clientInfoMapclientInfoMap=Context.getContext().getClientInfo();newThread(newRunnable(){@Overridepublicvoidrun(){//此时的clientInfoMap由于是在新线程创建前获取的,肯定是有值的Stringversion=clientInfoMap.get(\"version\");//以下正常逻辑....}}).start();
2.只需把 ThreadLocal 换成 InheritableThreadLocal,如下
publicfinalclassContext{privatestaticfinalInheritableThreadLocalLOCAL=newInheritableThreadLocal(){protectedContextinitialValue(){returnnewContext();}};publicstaticContextgetContext(){return(Context)LOCAL.get();}}newThread(newRunnable(){@Overridepublicvoidrun(){//此时的clientInfo能正常获取到MapclientInfo=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 用的是 threadLocals 变量存储的
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 的操作,至此真相大白
总结
看完本文,相信大家对 Threadlocal 与 InheritableThreadLocal 的使用及其底层原理的掌握已不存在疑问,这也提醒我们熟练地掌握一个组件或一项技术最好的方式还是熟读它的源码,毕竟源码之下无秘密,当我们使用到别人封装好的组件或类时,如果有兴趣也可以也看一下它的源码,以本文为例,其实我们工程中多处地方都使用了 Context.getContext().getClientInfo();
这样的获取客户端信息的形式,用惯了导致在多线程环境下没有引起警惕,以致踩了坑。
另外需要注意的是 ThreadLocal 使用不当可能导致内存泄漏,需要在线程结束后及时 remove 掉,这些技术细节不是本文重点,故而没有深入详解,有兴趣的大家可以去查阅相关资料
欢迎学编程的朋友们加入我的,我会 1 对 1 解决你的问题,直播带你做出项目、为你定制学习计划和求职指导,还能获取海量编程学习资源,和上万名学编程的同学共享知识、交流进步。
往期推荐
推荐
- 这个线上事故,直接把年终奖给干没了。。
- 比 Siri、Alexa 等更智能,开发者利用 GPT-3 开发语音助手
- 全球热头条丨为何公司宁愿给新人开高工资,却不愿意给知根知底的老员工涨薪?
- 中国足协原秘书长刘奕涉嫌严重违法 接受审查调查-全球热闻
- 银禧科技(300221)1月19日主力资金净买入65.19万元-天天微头条
- 鸡兔同笼c语言用for语句_鸡兔同笼c语言代码-热闻
- 世界热推荐:1月19日河南地区萤石市场价格暂稳
- 抗原自测板块1月18日跌0.26%,*ST 科华领跌,主力资金净流出1.8亿元
- 全球新动态:深圳银保监局今年首罚!广东南粤银行深圳分行涉多项违规
- 全球今日讯!2023云南河口消费券发放时间是几点?
- “乙类乙管”实施10天 上海浦东国际机场口岸客流环比上升约60% 每日热门
- 消息!城镇职工养老保险和居民养老保险的差别
- 艾德金融:中国黄金下游需求回暖 今晚关注回调幅度
- 同济留学生写春联、送福字,在杨浦体验传统中国年 | 爱申活暖心春
- 金 融 街(000402):2023年度第一期中期票据发行情况公告 天天新动态
- 得邦照明1月17日盘中涨停
- 券商观点|食品饮料行业:春节返乡热将带动下线城市和乡镇消费_播资讯
- 理性消费 注重安全 世界热资讯
- 环球快资讯:春节工会慰问职工会计分录是什么
- 莱茵生物:目前公司终端品牌暂未推出牙膏类产品
- 佛山照明董秘回复:公司光触媒灯具产品已通过广东省微生物分析检测中心进行病毒灭活测试
- 视频号直播SOP手册|天天观速讯
- 实时高速路况:河南因冰雪管制高速解禁 热门看点
- 世界讯息:(新春见闻)宠物经济新春再升温 线上线下掀“吸金”热潮
- 焦点精选!外交部发言人:中国疫苗是安全、有效的
- 生发液到底有用吗多久有效果?口碑最好的防脱洗发水育发液排名
- 智微智能:2023年2月6日召开第一次临时股东大会 审议3项议案 世界消息
- 2022年西藏修复退化草原437.5万亩 生态环境明显改善_独家焦点
- 2023无锡经开区春节停车免费时间+路段
- 焦点热讯:iPhone偷偷上架看片神器,全网资源免费看,这波操作把我看懵了
- 金陵药业股东户数下降10.46%,户均持股8.5万元 世界热消息
- 天天信息:毕业留言逗比句子(必备529句)
- 酒业是否有泡沫 报表繁荣年年高增长透视出什么本质? 世界今头条
- 【环球聚看点】今日NBA:约老师三双大胜湖人,威少手指脱臼,名记坚信公牛夺冠
- 【全球速看料】广东立项28个基础教育高质量发展实验区示范区
- 2023成都国色天乡一期童话世界春节活动时间+玩法_微速讯
- 真我GTNeo5外观和配置曝光:首发240W快充,2月登场-全球通讯
- 廊坊非法同居律师收费明细-全球热推荐
- 哈焊华通:融资净买入2.3万元,融资余额3810.93万元(01-10)_世界动态
- 2023年创业项目排行榜前十名(2023年1月10日更新)_实时焦点
- 天天热消息:无锡地铁4号线试乘券可以免费乘坐公交吗?
- 环球微动态丨柴达木农畜产品亮相浙江农博会
- 宋元崖门海战文化旅游区门票多少钱?
- 肇事逃逸的具体判刑标准是怎样的 全球热推荐
- 华瓷股份:公司酒瓶收入占总收入的比例大约20%左右
- 【世界热闻】长寒假期间如何构建和谐亲子关系?成都成华十万家长共上一堂课
- 波司登(03998.HK)早盘高开逾5%,截至发稿,涨5.41%,报4.48港元,成交额735.14万港元
- 2022张家界三级工伤赔偿期限多久-环球看热讯
- 天天快讯:又土又丑却年年爆单,她做了8年这种衣服 粉丝加钱都不一定能买到
- 每日冲煞属相 2023年11月18日属什么生肖冲什么_世界热点评
- 焦点消息!火箭科学和官僚主义——2022年国外航天发射的一些事件回顾
- 2023年时尚界十大趋势 世界资讯
- 北极甜虾能生吃吗 北极甜虾能不能生吃-最资讯
- 自在武威让文物"活:起来 让文创"潮"起来
- 光华营地打造“产品+营地”双轨运营 营地教育赛道再添新力量
- 白菜炒木耳怎么不出水?
- 退休钱怎么投资?世界最大教育养老金的启示_环球播报
- 2022年11月frm考试成绩未达预期怎么办?可以复查成绩吗?
- 宁夏银川规模以上工业总产值突破3000亿元_焦点速看
- 北新路桥签订4.2亿工程合同 天天观察
- 全球微速讯:保护市民财产飞奔抓贼,摔伤骨折的他说:这点痛不算什么
- 知乎往返于洞穴
- 1月18日首发 Winnebago电动房车预告
- 梦幻西游养号必学的小技巧,能快速赚取储备金的师徒任务_每日焦点
- 2023年春节电影上映一览表(档期+主演) 即时看
- 每日速递:2023上海各大高校寒假放假时间汇总
- 黔源电力:目前,登记公司还未发布上述信息 环球热闻
- 走好国防科技拔尖创新人才自主培养之路
- 青岛一周新房成交2855套,绿地国科健康科技小镇交付面临再延期 环球热头条
- 赛福天: 第四届董事会第二十一次会议决议公告_快资讯
- 湖湘自然历丨郴州风物:壮阔熊峰,万山来朝 世界百事通
- 2023张家界春节免门票活动(时间+景区名单+对象)
- 今日精选:晶瑞电材:公司是一家微电子材料的平台型高新技术企业 围绕泛半导体材料和新能源材料两个方向
- 环球快播:超350万人预约爱奇艺《浮图缘》,12月27日上线连更8天
- 世界快报:李家超:确保在今年内带领香港脱离疫情困扰 回复正常
- Nacos 支持达梦数据库持久化插件使用 环球时讯
- 偷税漏税罚款怎么交?|环球速看料
- 事关老人、孕妇和儿童,重点人群恢复期健康指导来了!|每日观察
- 全球聚焦:观众讨厌,他们却戏约不断,这5位败光路人缘的演员,什么来头?
- 热门看点:高安市妇幼保健院:全力打造妇幼特色品牌
- 【全球播资讯】祥鑫科技荣列2022年广东省制造业500强
- 本周盘点(12.26-12.30):浪潮软件周涨0.07%,主力资金合计净流出2745.81万元_要闻速递
- 从“一喜”到“二喜”:名气有了,真诚少了-世界实时
- 正味集团(02147.HK)拟发售2亿股股份 预计1月13日上市 天天报道
- 全球资讯:汉秀剧场元旦演出时间及门票2023
- 镇平彭营镇:积极重塑行政审批制度 奋力优化政务服务环境
- 4G手机仍然和5G分庭抗礼
- 环球观热点:一博科技拟不低于25亿元投建研发运营与智能制造总部项目
- 焦点热文:拓尔思:12月28日融券卖出金额31.45万元,占当日流出金额的0.73%
- 羊没羊,好像也没那么重要了!|天天播资讯
- 全球短讯!上汽通用武汉奥特能超级工厂竣工投产,Electra E5试装车下线
- 天天实时:从凉茶到新药:香港中医药新生态
- 今热点:【券商聚焦】高盛维持联想集团(00992)中性评级 料明年仍处于个人电脑下行周期
- 为什么熟腰果比生腰果便宜?
- 每日消息!荣联科技: 独立董事关于第六届董事会第二十二次会议相关事项的事前认可意见
- 超越ConvNeXt!Transformer 风格的卷积网络视觉基线模型Conv2Former 世界最资讯
- 焦点速讯:为什么煮汤圆会粘底?
- 滴水贷款逾期26年会上征信系统吗?
- [快讯]三孚新科:民生证券股份有限公司广州三孚新材料科技股份有限公司部分募投项目延期的核查意见 焦点速看
- 老舍语言的魅力
X 关闭
行业规章
X 关闭