范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

多线程引发的惨案直接把年终给干没了

  前些日子我们线上出现了一个比较严重的故障,这个故障是多线程使用不当引起的,挺有代表性的,所以分享给大家,希望能帮大家避坑 问题简述
  先简单介绍一下问题产生的背景,我们有个返利业务,其中有个搜索场景,这个场景是用户在 app 输入搜索关键词,然后 server 会根据这个关键词到各个平台(如淘宝,京东,拼多多等)调一下搜索接口,聚合这些搜索结果后再返回给用户,最开始这个搜索场景处理是单线程的,但随着接入的平台越来越多,搜索请求耗时也越来越长,由于每个平台的搜索请求都是独立的,很显然,单线程是可以优化为多线程的,如下
  img
  这样的话,搜索请求的耗时就只取决于搜索接口耗时最长的那个平台,所以使用多线程显然对接口性能是一个极大的优化,但使用多线程改造上线后,短时间内社群中有多名用户反馈前台展示「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.无锁化提升并发性能
  先说第一个,无锁化提升并发性能,影响并发的原因有很多,其中一个很重要的原因就是锁,为了防止对共享变量的竞用,不得不对共享变量加锁
  如果对共享变量争用的线程数增多,显然会严重影响系统的并发度,最好的办法就是使用"影分身术"为每个线程都创建一个线程本地变量,这样就避免了对共享变量的竞用,也就实现了无锁化
  无锁化
  ThreadLocal 即线程本地变量,它可以为每个线程创建一份线程本地变量,使用方法如下 static ThreadLocal threadLocal1 = new ThreadLocal() {     @Override     protected SimpleDateFormat initialValue() {         return new SimpleDateFormat("yyyy-MM-dd");     } };  public String formatDate(Date date) {     return threadLocal1.get().format(date); }
  这样的话每个线程就独享一份与其他线程无关的 SimpleDateFormat 实例副本,它们调用 formatDate 时使用的 SimpleDateFormat 实例也是自己独有的副本,无论对副本怎么操作对其他线程都互不影响
  通过以上例子我们可以看出,可以通过 new ThreadLocal  + initialValue   来为创建的 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> threadLocal        = new ThreadLocal<>();      @Override     public void run() {                 // clientInfo 初始化         Map 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 LOCAL = new ThreadLocal() {         protected Context initialValue() {             return new Context();         }     };         private Long uid;     // 用户uid       private Map clientInfo; // 客户端信息       private Map headers = null; // 请求头信息       private Map> cookies = null; // 请求 cookie        public static Context getContext() {         return (Context) LOCAL.get();     }  }
  这样的话我们可通过 Context.getContext().getXXX()   的形式来获取线程所需的信息,通过这样的方式我们不仅避免了定义无数 ThreadLocal 变量的烦恼,而且还收拢了上下文信息的管理
  通过以上介绍相信大家也都知道了 clientInfo 其实是借由 ThreadLocal 存储的,认清了这个事实后那我们现在再回头看开头的生产问题:将单线程改成多线程后,为什么在新线程中就拿不到 clientInfo 了? 问题剖析
  源码之下无秘密,我们查看一下源码来一探究竟,获取本地变量的值使用的是 ThreadLocal.get 方法,那就来看下这个方法 public class ThreadLocal {         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 方法主要步骤如下 首先需要获取当前线程 其次获取当前线程的 ThreadLocalMap 进而再去获取相应的本地变量值 如果没有的话则调用 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 LOCAL = new InheritableThreadLocal() {         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 extends ThreadLocal {      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 掉,这些技术细节不是本文重点,故而没有深入详解,有兴趣的大家可以去查阅相关资料 来源:https://mp.weixin.qq.com/s/ObXr7SR9TJC6aqTI-OYlqA
  作者:坤哥

青春有梦正当时何为少年?他们谈风雨不惧,论未来不忧。他们追逐梦想,他们无所畏惧,只因他们是少年。青少年是早晨八九点钟的太阳,青少年最有朝气,是国家的未来,民族的希望,青春逢盛世,追梦正当时。青春唐嫣交给时间就好所谓人生开挂,不过是厚积薄发,坚持你所热爱的,热爱你所坚持的,剩下的,交给时间就好!不是每一个人,都有机会长得漂亮,可是每一个人,都有机会活的漂亮,人生需要一种信念,生活需要一种态景德镇陶瓷大学和井冈山大学,江西这两所大学哪个实力更胜一筹?江西省位于我国中部省份,也是华中地区的重要经济省份,江西历史文化悠久,名胜古迹众多,也是全国非常知名的旅游省份,当然江西也有很多本科院校,尤其是省内排名第一的南昌大学,是国内2112022年令人期待的长江游轮航线上海至重庆船票的详细介绍说到2022年令人期待的长江游轮航线,那就是从上海坐船到重庆的游轮航线。其实有很多粉丝朋友经常问我,2022年有没有从上海到重庆的游轮航线呢?小潘在这里告诉大家,2022年本来是有世界上飞行时间最长的一个航班,在空中将近19个小时,中间不转机随着社会的发展,人民的生活水平不断提高,交通也越来越发达,人们出门的时候,可以使用的交通工具也变得更多了。自从有了飞机,很多人都可以去远一点的地方旅行,也可以节省不少的时间。有了飞大魔王张怡宁罕见同框郭晶晶,二人同为豪门阔太,气质却截然不同衣着穿搭其实是一个女人衣品和气质的见证。服装是人的一个门面,这个门面的搭配决定着个人的俏皮和靓丽,也就是可以说通过穿搭体现自我的品位,通过服装呈现自我的优势和形象,所以服装很重要。农村俗语多病长寿,命硬福寡,古人的经验智慧,有道理吗?立冬时节的北方农村,几位老人正围坐在村口的大槐树下,享受着午后的温暖阳光。他们在一起谈论着秋收秋种情况,从他们欢声笑语的谈话间,听到一句农村俗语老话多病长寿,命硬福寡是什么意思?有泪目!瓦妮莎再与科比壁画合影,盛装打扮太漂亮,守寡3年不改嫁作为NBA富婆,瓦妮莎的一举一动都受到了网友的关注,最近大家都在过万圣节,瓦妮莎也不例外,特意给自己装扮的非常漂亮,如同一个女神一般,街上不少人的目光都放到了瓦妮莎身上,但她并不在开花劳模龙吐珠,可盆栽可爬藤,养护简单适合新手今天给大家推荐一种好养的懒人花卉,两广地区的花友称它为开花劳模,他就是龙吐珠。龙吐珠不仅开花勤快,而且花量大,冬季保持温暖可四季常开。龙吐珠简介龙吐珠是马鞭草科灌木,大苗具有较强的恭喜艾克森!巴西神锋结束巴乙征程,有望返回中国帮助广州队保级本赛季的巴西乙级联赛已经结束,上赛季意外降级的传统豪门格雷米奥重新回到了巴甲联赛行列,这其中最让中国球迷感到关心的是,这个球队的中国归化球员艾克森,他在上赛季开始之前来到这个球队,保存白菜很简单,牢记3点,放一冬也新鲜,不烂菜帮不黄叶不长芽秋日生活打卡季俗话说立冬白菜赛羊肉,立冬之后,家家户户都会开始储存大白菜了,东北人买白菜都是大车小车拉回家储存起来,一个冬天都不会为了蔬菜而发愁了。之前是因为物质匮乏,而如今超市里
当一个人这样回复你,心里不会拒绝你的爱爱情是非常珍贵的,在很多时候我们一定要知道该如何去考虑不要总是想太多,有些时候就是这样子的,你想的太多是没有用的也不一定能够解决问题,所以有些事我们要明白。男女之间有时候看起来没有天下文章艾在人间自拍(冬日暖阳)天下是所有人的天下,非一己或一撮人的天下。文章虽表达的是个人的心声,但输出的却不应仅仅是个人的三观(价值观,道德观与人生观)。真的文章,敢于正视残酷的现实,6种生命形态,碳基生命仅排第三,排名第一的硅基生命有多可怕?综述关于宇宙中所生存的生命形态,你了解几种?生命,是一个近乎奇迹般的存在。由不同物质所产生反应,地球在漫长的时间进化中出现了原始的生命。以碳元素为基础,地球上诞生了多种多样的碳基生真的有外星生命吗外星生命,指存在于地球以外的生命体。这个概念囊括了简单的细菌到具有高度智慧的外星人。研究和测试关于外星生命猜想的学科被称作地外生物学或天体生物学(从天文视角研究地球生命也属于天体生新发现两颗超级地球!距我们仅15。8光年,唯一担心的是早有生命既然太阳拥有行星,那宇宙中其他的恒星也很可能拥有自己的行星,虽然这是一种很合理的推测,但想要找到其他恒星的行星却不容易,如果将一颗恒星比作一座闪亮的灯塔,那它的行星就大概是灯塔下的生命本就脆弱和不完美生命本就脆弱和不完美,我们生于尘埃,终归于尘埃。我们拥有的和我们缺失的,令我欢喜的,令我们无奈的,都是生命本质。美丽财富就智慧年老成熟和孩子气都是正当的,而且这就是世界。所有我们身人到中年不得不苟且,流浪的独孤老狼书写平凡中的不平凡为什么叫自己流浪的独孤老狼而不是流浪的孤独老狼?从孤独到独孤,不单是语序和上口,境界气势立现。孤独是longly,独孤是along孤独是自怨自艾,独孤是我自横刀孤独是为人人为我,独付过千般爱,莫生千般恨情感点评大赏一个人分手后的三个十年再三次被曾经的恋人伤害,那决不是愚蠢愚昧无知,而是病入膏肓无可救药。世间有一通百病的药方,却难寻千方能治的情伤。天意缘来缘尽就让它灰飞烟灭,付出千浴冰重生来自一个标本的自白传说,凤凰是人世间幸福的使者。每五百年,它就要背负着积累于人世间的所有不快和仇恨恩怨,投身于熊熊烈火中自焚,以生命的美丽终结换取人世的祥和与幸福。在肉体经受了巨大的痛苦和轮回后它们故乡的年味(一)娱兔迎春曾经仗剑走天涯,去看一看世界的繁华不知不觉离开家乡已经多年,从青葱岁月的求学时代开始,当时觉得很开心离开家乡,一直以为自己会成功,头也不回便离开了故乡。当时的心情就觉得未来连续7天超1亿元!这里的免税店,又火了!12月初,海南省启动了为期两个月的海南离岛免税跨年狂欢季,通过抽奖打折满减等方式,全面促消费。全球精品(海口)免税城有限公司总经理助理刘加我们联合航司机场渠道合作伙伴,向广大旅客发