前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >多线程引发的惨案直接把年终给干没了

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

作者头像
kunge
发布于 2023-03-08 07:37:52
发布于 2023-03-08 07:37:52
31800
代码可运行
举报
文章被收录于专栏:码海码海
运行总次数:0
代码可运行

问题简述

先简单介绍一下问题产生的背景,我们有个返利业务,其中有个搜索场景,这个场景是用户在 app 输入搜索关键词,然后 server 会根据这个关键词到各个平台(如淘宝,京东,拼多多等)调一下搜索接口,聚合这些搜索结果后再返回给用户,最开始这个搜索场景处理是单线程的,但随着接入的平台越来越多,搜索请求耗时也越来越长,由于每个平台的搜索请求都是独立的,很显然,单线程是可以优化为多线程的,如下

img

这样的话,搜索请求的耗时就只取决于搜索接口耗时最长的那个平台,所以使用多线程显然对接口性能是一个极大的优化,但使用多线程改造上线后,短时间内社群中有多名用户反馈前台展示「APP 需要升级的提示」,经定位后发现是因为在多线程中无法获取客户端信息,由于客户端信息缺失,导致返回给用户需要升级的提示,伪代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 开启多线程处理
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. 无锁化提升并发性能
  2. 简化变量的传递逻辑
1.无锁化提升并发性能

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

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

无锁化

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
static ThreadLocal<SimpleDateFormat> threadLocal1 = new ThreadLocal<SimpleDateFormat>() {
    @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 时被调用以初始化本地变量)。当然,如果之后需要修改本地变量的话,也可以用以下方式来修改

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
threadLocal1.set(new SimpleDateFormat("yyyy-MM-dd"))

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 这个内部类中的,所以可在线程调用的任意方法中取出,伪代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocalWithUserContext implements Runnable {

    private static ThreadLocal<Map<String,String>> threadLocal 
      = new ThreadLocal<>();

    @Override
    public void run() {
                // clientInfo 初始化
        Map<String, String> 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 等信息就作为此上下文对象的属性即可,代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final class Context {

    private static final ThreadLocal<Context> LOCAL = new ThreadLocal<Context>() {
        protected Context initialValue() {
            return new Context();
        }
    };


      private Long uid;     // 用户uid
      private Map<String, String> clientInfo; // 客户端信息
      private Map<String, String> headers = null; // 请求头信息
      private Map<String, Map<String, String>> cookies = null; // 请求 cookie

      public static Context getContext() {
        return (Context) LOCAL.get();
    }

}

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

通过以上介绍相信大家也都知道了 clientInfo 其实是借由 ThreadLocal 存储的,认清了这个事实后那我们现在再回头看开头的生产问题:将单线程改成多线程后,为什么在新线程中就拿不到 clientInfo 了?

问题剖析

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocal<T> {
        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 方法主要步骤如下

  1. 首先需要获取当前线程
  2. 其次获取当前线程的 ThreadLocalMap
  3. 进而再去获取相应的本地变量值
  4. 如果没有的话则调用 initiaValue 方法来初始化本地变量

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

解决方案

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 先从当前线程的 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,如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final class Context {
    private static final InheritableThreadLocal<Context> LOCAL = new InheritableThreadLocal<Context>() {
        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 长啥样

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    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 变量存储的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Thread implements Runnable {
    // ThreadLocal 实例的底层存储
    ThreadLocal.ThreadLocalMap threadLocals = null;

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 掉,这些技术细节不是本文重点,故而没有深入详解,有兴趣的大家可以去查阅相关资料

·············· END ·············

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码海 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
ThreadLocal及InheritableThreadLocal的原理剖析
我们知道,线程的不安全问题,主要是由于多线程并发读取一个变量而引起的,那么有没有一种办法可以让一个变量是线程独有的呢,这样不就可以解决线程安全问题了么。其实JDK已经为我们提供了ThreadLocal这个东西。
Java学习录
2019/04/18
5730
ThreadLocal和InheritableThreadLocal深入分析
  通过ThreadLocal和InheritableThreadLocal,我们能够很方便的设计出线程安全的类。JDK底层是如何做到的呢?ThreadLocal和InheritableThreadLocal有什么区别呢与联系呢?为什么有了ThreadLocal类还需要InheritableThreadLocal类,他们与Thread类是什么关系?带着这些问题我们来分析他们的源码。
良辰美景TT
2018/09/11
8620
ThreadLocal和InheritableThreadLocal深入分析
宇智波程序笔记8-【高并发】ThreadLocal学会了这些,你也能和面试官扯皮了!
作者个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了精准定时调度方案,经受住了生产环境的考验。为使更多童鞋受益,现给出开源框架地址:
不会飞的小鸟
2020/08/26
2960
ThreadLocal:Java中的影分身
老套路,先列举下关于ThreadLocal常见的疑问,希望可以通过这篇学习笔记来解决这几个问题:
阿杜
2019/01/03
3760
一文说清楚ThreadLocal
当多个线程对同一变量进行写操作的时候,容易出现线程安全问题,所以就会用到对应的锁和其他一些方法,我们先不介绍锁,先介绍ThreadLocal, ThreadLocal字面意思本地线程,ThreadLocal使每个线程之间是隔离的,数据是独立的,我们使用过session都知道 session是一个会话,我们可以用它来存储一些用户的基本信息,这样每个用户在服务端都能取到,ThreadLocal也可以做到,ThreadLocal将相应的信息存储在当前的线程中,只有当前线程能够访问,其他线程不能访问,这样就能保证线程安全,其实ThreadLocal是一个定制化的Map。
小四的技术之旅
2022/07/26
3180
ThreadLocal父子线程数据传递方案
介绍InheritableThreadLocal之前,假设对 ThreadLocal 已经有了一定的理解,比如基本概念、原理。在讲解之前我们先列举有关ThreadLocal的几个关键点。
Bug开发工程师
2018/07/23
1.6K1
ThreadLocal父子线程数据传递方案
在 Spring Security 中,我就想从子线程获取用户登录信息,怎么办?
大家知道在 Spring Security 中想要获取登录用户信息,不能在子线程中获取,只能在当前线程中获取,其中一个重要的原因就是 SecurityContextHolder 默认将用户信息保存在 ThreadLocal 中。
江南一点雨
2020/07/21
4.9K0
在 Spring Security 中,我就想从子线程获取用户登录信息,怎么办?
ThreadLocal父子线程数据传递方案(修正篇)
介绍InheritableThreadLocal之前,假设读者对 ThreadLocal 已经有了一定的理解,比如基本概念、原理等。在讲解之前我们先列举有关ThreadLocal的几个关键点。
Bug开发工程师
2018/07/23
6.3K0
ThreadLocal父子线程数据传递方案(修正篇)
探索JAVA并发 - ThreadLocal
SimpleDateFormat是我们常用的日期格式化工具,但熟悉的朋友都知道它是线程不安全的。
acupt
2019/08/26
3950
一篇文章看懂 ThreadLocal 原理,内存泄露,缺点以及线程池复用的值传递问题
编辑:业余草 来源:https://www.xttblog.com/?p=4946 一篇文章看懂 ThreadLocal 原理,内存泄露,缺点以及线程池复用的值传递问题。 ThreadLocal 相信
业余草
2020/04/08
4.3K0
一篇文章看懂 ThreadLocal 原理,内存泄露,缺点以及线程池复用的值传递问题
你问这谁会啊?ThreadLocal 父子线程之间该如何传递数据?
忘记之前是哪个公司面试的时候问到的,并不是一个常见的问题,我当时也没回答正确,就按照线程通信那一套比如什么 synchronized、Locks、volatile 啥的 XJB 说的,面试完找了些资料今天整理了下分享给大家~
飞天小牛肉
2023/01/16
5230
ThreadLocal全面解析
冬天vs不冷
2025/01/21
1250
ThreadLocal全面解析
java并发编程学习: ThreadLocal使用及原理
多线程应用中,如果希望一个变量隔离在某个线程内,即:该变量只能由某个线程本身可见,其它线程无法访问,那么ThreadLocal可以很方便的帮你做到这一点。  先来看一下示例: package yjmyzz.test; public class ThreadLocalTest1 { public static class MyRunnable implements Runnable { private ThreadLocal<Integer> threadLocal = new
菩提树下的杨过
2018/01/19
5990
ThreadLocal 类
ThreadLocal 并不是一个Thread,而是 ThreadLocalVariable(线程局部变量)。也许把它命名为 ThreadLocalVar更加合适。线程局部变量就是为每一个使用该变量的线程都提供一个变量值的副本,是 Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。ThreadLocal是除了加锁这种同步方式之外的另一种保证多线程访问出现线程不安全的方式。
Java架构师必看
2021/05/14
4990
ThreadLocal 类
java(8)--线程ThreadLocal详解
在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。 在JDK5.0以后,ThreadLocal已经支持泛型,ThreadLocal类的类名变为ThreadLocal<T>。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
黄规速
2022/04/14
3.7K0
java(8)--线程ThreadLocal详解
研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?
    今天和一个之前研二的学妹聊天,聊及她上周面试字节的情况,着实感受到了Java后端现在找工作的压力啊,记得在18,19年的时候,研究生计算机专业的学生,背背八股文找个Java开发工作毫无问题,但现在即便你是应届生,问的考题也非常的深入和细节了,只会背八股,没有一定的代码量和项目积累,根本找不到像样的工作,具体聊天内容如下:
JavaBuild
2024/05/27
880
研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?
【JUC基础】14. ThreadLocal
一般提到多线程并发总是要说资源竞争,线程安全。而通常保证线程安全的其中一种方式便是控制资源的访问,也就是加锁。其实还有另一种方式,那么便是增加资源来保证所有对象不竞争少数资源。比如,有100个人需要填写信息表,如果只有一只笔,那么要么变成串行,一个一个填写,要么就是我写一半你写一半。那么如果准备100只笔,100个人每个人都有一只笔能够填写信息表,那么就不会出现竞争的情况,也就能顺利的保证信息表的填写。这支笔也就是我们今天要说的ThreadLocal。
有一只柴犬
2024/01/25
1600
【JUC基础】14. ThreadLocal
InheritableThreadLocal源码阅读
在进行多线程编程时,我们经常需要线程池子线程和父线程进行ThreadLocal信息传递,实现一些业务处理。 先看一个例子 public class App { static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(); public static void main(String[] args) { threadLocal.set(new Integer(123)); Thread
春哥大魔王
2018/04/17
6410
ThreadLocal全解析——你想要的这里都有
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以通过ThreadLocal对象查询到绑定在这个线程上的一个值。
用户5325874
2021/12/07
4770
ThreadLocal全解析——你想要的这里都有
ThreadLocal案例分析
要理解为什么需要ThreadLocal就不得不从线程安全问题说起。高并发是很多领域都会遇到的非常棘手的问题,其最核心的问题在于如何平衡高性能和数据一致性。当我们说某个类是线程安全的时候,也就意味着该类在多线程环境下的状态保持一致性。
topgunviper
2022/05/12
4610
ThreadLocal案例分析
相关推荐
ThreadLocal及InheritableThreadLocal的原理剖析
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文