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

你的也是我的,ko多线程的3个例子,局部变量透传

时间:2023-03-12 23:58:00 科技观察

javathreadlocal绑定到线程。你在一个线程中设置的值无法在另一个线程中获取。如果在threadlocal的并行线程中创建了一个新的子线程,那么其中的值就不能被传递或者共享(往下看之前想想为什么)。这就是透传问题。你可以把线程间透传值看成是一个bug。这些问题一般都是隐藏的,但是当问题暴露出来的时候,脾气就比较火爆,让人心慌,怀疑人生。作为代码的掌舵人,我们绝不能容忍这种问题的肆虐。这篇文章适合仔细看看。我们举3个例子,说明通过编码手段解决这类bug的一般方法,希望能达到举一反三的效果。对于搞基建的同学来说,是必备的知识点。1.普通线程的ThreadLocal透传问题2.sl4jMDC组件中的ThreadLocal透传问题3.Hystrix组件的透传问题涉及到的代码比较多,所以xjjdog把这三个例子的代码放到了github和想深入研究,可以下载调试。https://github.com/xjjdog/example-pass-through1.问题的简单演示为了更直观的理解,下面展示一段异常代码。上面的代码在主线程中设置了一个简单的线程局部变量,然后想在自线程中取回它的值。执行后发现程序的输出为:null。该程序的输出与我们的预期大相径庭。其实把ThreadLocal换成InheritableThreadLocal就可以了。不要高兴得太早。对于使用线程池的情况,因为线程会被缓存,线程被缓存重复使用。这时候父子线程关系的上下文传递是没有意义的。2、解决线程池透传问题所以,线程池InheritableThreadLocal提交,获取的值可能在上一个任务执行完后还剩下,这是错误的。只有在执行任务时才使用的正常功能。上面的问题,transmittable-thread-local项目已经很好的解决了,并且提供了java-agent支持。下面从最小合集的源码层面来看一下内容。首先我们来看一下ThreadLocal的结构。ThreadLocal实际上作为一个键存在于一个Map中。这个Map就是ThreadLocalMap,它以私有变量的形式存在于Thread类中。以上图为例,如果我创建一个ThreadLocal,然后调用set方法,它会先找到当前线程,再找到threadLocals,最后把自己作为key存入这个map。hreadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);map.set(this,value);为了能够完成多线程的协调工作,必须提供全套的多线程工具。包括但不限于:1.定义注解,注解修饰的ThreadLocal类定义一个新的ThreadLocal类,这样在赋值的时候,可以根据注解进行拦截过滤。这就需要在定义ThreadLocal的时候,使用我们提供的ThreadLocal类,而不是jdk提供的两个。2.父子线程之间拷贝数据。在线程池提交任务之前,我们需要一个地方来临时存放父进程的ThreadLocal内容。由于很多变量都是私有的,所以需要根据反射来操作。根据上面提供的ThreadLocal类的结构,我们需要直接操作变量表(这也是jdk不能随便改变量名的原因)。父线程相关的变量暂存后,可以通过主动赋值,使用时清零来复制变量。3.提供一个专门的Callable或者Runnable,那么这些数据是怎么组装的呢?还是靠我们的任务载体类。线程池提交线程,一般通过Callable或者Runnable。以Runnable为例,我们看一下调用关系。以下类采用委托模式。这样,我们只要在提交任务的时候使用我们自定义的Runnable;同时使用自定义的ThreadLocal,可以正常完成透传。3.解决MDC透传问题。sl4j的MDC机制非常好。它通常用于在线程本地保存“诊断数据”,然后由日志组件打印。内部基于threadLocal实现;但是,存在一些问题。主线程中设置的MDC在其子线程(多线程池)中无法获取到数据。下面将介绍如何解决这个问题。!MDC(MappedDiagnosticContexts),这是一个用于存储诊断日志的线程安全容器。通常,请求的唯一标识,如sessionId,在请求被处理之前被放置在MDC容器中。这个唯一标识符将与日志一起输出。配置文件可以使用占位符进行变量替换。和上面介绍的方法类似,我们需要提供专用的Callable和Runnable。另外,为了同时支持MDC和普通线程,这两个类采用了装饰器模式来增加功能。就单个类而言,对外呈现仍然是委托模式。相同的想法,相同的模型。不同的是,父线程的信息是暂存的,我们直接使用MDC的内部方法,在任务执行前后进行相应的操作。4.解决Hystrix透传问题同样的问题在Netflix的熔断组件Hystrix中依然存在。在Hystrix线程池模式下,需要修改透传ThreadLocal,它自己无法完成这个功能。但是Hystrix策略不能简单的通过yml文件来配置。我们参考了SpringCloud中这个策略的扩展方法来开发自己的策略。需要继承HystrixConcurrentStrategy。构建代码还是比较长的,可以查看github项目。但是有一个地方需要说明一下。我们使用装饰器模式将代码一层层嵌套,并为其添加多线程透传功能和MDC传输功能。这样我们班就同时具备了以上三种环境下的透传功能。同样的End思路可以用在其他组件上。比如我们在很多调用链的文章中都提到了trace信息在多线程环境下的传递。一般都是将数据暂时存放在当前线程中,等任务提交的时候再打包。值得注意的是,这种方法侵入性比较强,适合打包在一个通用的基础工具包中。如果在业务中这么用,大概率会被骂死。我能做些什么。ThreadLocal会导致许多棘手的错误并导致代码污染。在使用它之前,请确保您确实需要使用它。例如,如果您在SimpleDateFormat类上使用线程局部变量,则可以将其替换为DateTimeFormatter。我们不擅长解决问题,我们只擅长解决容易出问题的类。作者简介:品味小姐姐(xjjdog),一个不允许程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。我的个人微信xjjdog0,欢迎加好友进一步交流。