全链路压测必备基础组件之线程上下文管理之“三剑客”,java面试项目中遇到的难点

本文详细解析了ThreadLocal的复制机制,比较了InheritableThreadLocal的浅拷贝特点,以及在处理线程池时可能出现的问题。重点介绍了TransmittableThreadLocal的出现,解决了InheritableThreadLocal在线程池中的局限性,展示了其实现原理和使用场景。
摘要由CSDN通过智能技术生成

private ThreadLocalMap(ThreadLocalMap parentMap) {

Entry[] parentTable = parentMap.table;

int len = parentTable.length;

setThreshold(len);

table = new Entry[len];

for (int j = 0; j < len; j++) {

Entry e = parentTable[j];

if (e != null) {

@SuppressWarnings(“unchecked”)

ThreadLocal key = (ThreadLocal) e.get();

if (key != null) {

Object value = key.childValue(e.value);

Entry c = new Entry(key, value);

int h = key.threadLocalHashCode & (len - 1);

while (table[h] != null)

h = nextIndex(h, len);

table[h] = c;

size++;

}

}

}

}

上述代码就不一一分析,类似于Map的复制,只不过其在Hash冲突时,不是使用链表结构,而是直接在数组中找下一个为null的槽位。

温馨提示:子线程默认拷贝父线程的方式是浅拷贝,如果需要使用深拷贝,需要使用自定义ThreadLocal,继承InheritableThreadLocal并重写childValue方法。

2.3 验证InheritableThreadLocal的特性

验证代码如下:

public class Service {

private static InheritableThreadLocal requestIdThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {

Integer reqId = new Integer(5);

Service a = new Service();

a.setRequestId(reqId);

}

public void setRequestId(Integer requestId) {

requestIdThreadLocal.set(requestId);

doBussiness();

}

public void doBussiness() {

System.out.println(“首先打印requestId:” + requestIdThreadLocal.get());

(new Thread(new Runnable() {

@Override

public void run() {

System.out.println(“子线程启动”);

System.out.println(“在子线程中访问requestId:” + requestIdThreadLocal.get());

}

})).start();

}

}

执行结果如下:

在这里插入图片描述

符合预期,在子线程中如愿访问到了在主线程中设置的本地环境变量。

2.4 InheritableThreadLocal局限性

InheritableThreadLocal支持子线程访问在父线程的核心思想是在创建线程的时候将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时。但我们提到并发、多线程就理不开线程池的使用,因为线程池能够复用线程,减少线程的频繁创建与销毁,如果使用InheritableThreadLocal,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱,验证代码如下:

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class Service {

/**

  • 模拟tomcat线程池

*/

private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);

/**

  • 业务线程池,默认Control中异步任务执行线程池

*/

private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);

/**

  • 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。

*/

private static InheritableThreadLocal requestIdThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {

for(int i = 0; i < 10; i ++ ) { // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,

// 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量

tomcatExecutors.submit(new ControlThread(i));

}

//简单粗暴的关闭线程池

try {

Thread.sleep(10000);

} catch (InterruptedException e) {

e.printStackTrace();

}

businessExecutors.shutdown();

tomcatExecutors.shutdown();

}

/**

  • 模拟Control任务

*/

static class ControlThread implements Runnable {

private int i;

public ControlThread(int i) {

this.i = i;

}

@Override

public void run() {

System.out.println(Thread.currentThread().getName() + “:” + i);

requestIdThreadLocal.set(i);

//使用线程池异步处理任务

businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));

}

}

/**

  • 业务任务,主要是模拟在Control控制层,提交任务到线程池执行

*/

static class BusinessTask implements Runnable {

private String parentThreadName;

public BusinessTask(String parentThreadName) {

this.parentThreadName = parentThreadName;

}

@Override

public void run() {

//如果与上面的能对应上来,则说明正确,否则失败

System.out.println(“parentThreadName:” + parentThreadName + “:” + requestIdThreadLocal.get());

}

}

}

执行效果如下:

pool-1-thread-1:0

pool-1-thread-2:1

pool-1-thread-3:2

pool-1-thread-4:3

pool-1-thread-5:4

pool-1-thread-6:5

pool-1-thread-7:6

pool-1-thread-8:7

pool-1-thread-9:8

pool-1-thread-10:9

parentThreadName:pool-1-thread-7:6

parentThreadName:pool-1-thread-4:6

parentThreadName:pool-1-thread-3:6

parentThreadName:pool-1-thread-2:6

parentThreadName:pool-1-thread-1:6

parentThreadName:pool-1-thread-9:6

parentThreadName:pool-1-thread-10:6

parentThreadName:pool-1-thread-8:7

parentThreadName:pool-1-thread-6:5

parentThreadName:pool-1-thread-5:4

从这里可以出thread-7、thread-4、thread-3、thread-2、thread-1、thread-9、thread-10获取的都是6,在子线程中出现出现了线程本地变量混乱的现象,在全链路跟踪与压测出现这种情况是致命的。

问题:大家通过上面的学习,应该能解释这个现象?此处可以稍微停下来思考一番。

怎么解决这个问题呢?

TransmittableThreadLocal ”闪亮登场“。

3、TransmittableThreadLocal


3.1 TransmittableThreadLocal“何许人也”

TransmittableThreadLocal何许人也,它可是阿里巴巴开源的专门解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。实践是检验整理的唯一标准,我们还是以上面的示例来进行验证,看看TransmittableThreadLocal是否支持上述场景:

首先需要在pom.xml文件中引入如下maven依赖:

com.alibaba

transmittable-thread-local

2.10.2

示例代码如下:

public class Service {

/**

  • 模拟tomcat线程池

*/

private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);

/**

  • 业务线程池,默认Control中异步任务执行线程池

*/

private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4)); // 使用ttl线程池,该框架的使用,请查阅官方文档。

/**

  • 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。

*/

private static TransmittableThreadLocal requestIdThreadLocal = new TransmittableThreadLocal<>();

// private static InheritableThreadLocal requestIdThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {

for(int i = 0; i < 10; i ++ ) {

tomcatExecutors.submit(new ControlThread(i));

}

//简单粗暴的关闭线程池

try {

Thread.sleep(10000);

} catch (InterruptedException e) {

e.printStackTrace();

}

businessExecutors.shutdown();

tomcatExecutors.shutdown();

}

/**

  • 模拟Control任务

*/

static class ControlThread implements Runnable {

private int i;

public ControlThread(int i) {

this.i = i;

}

@Override

public void run() {

System.out.println(Thread.currentThread().getName() + “:” + i);

requestIdThreadLocal.set(i);

//使用线程池异步处理任务

businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));

}

}

/**

  • 业务任务,主要是模拟在Control控制层,提交任务到线程池执行

*/

static class BusinessTask implements Runnable {

private String parentThreadName;

public BusinessTask(String parentThreadName) {

this.parentThreadName = parentThreadName;

}

@Override

public void run() {

//如果与上面的能对应上来,则说明正确,否则失败

System.out.println(“parentThreadName:” + parentThreadName + “:” + requestIdThreadLocal.get());

}

}

}

其运行结果如下:

pool-1-thread-10:9

pool-1-thread-8:7

pool-1-thread-7:6

pool-1-thread-9:8

pool-1-thread-6:5

pool-1-thread-5:4

pool-1-thread-4:3

pool-1-thread-3:2

pool-1-thread-2:1

pool-1-thread-1:0

parentThreadName:pool-1-thread-5:4

parentThreadName:pool-1-thread-9:4

parentThreadName:pool-1-thread-3:4

parentThreadName:pool-1-thread-2:4

parentThreadName:pool-1-thread-7:4

parentThreadName:pool-1-thread-8:4

parentThreadName:pool-1-thread-1:4

parentThreadName:pool-1-thread-6:5

parentThreadName:pool-1-thread-10:9

parentThreadName:pool-1-thread-4:3

执行结果符合预期。那TransmittableThreadLocal是如何实现的呢?

3.2 TransmittableThreadLocal实现原理

从InheritableThreadLocal不支持线程池的根本原因是InheritableThreadLocal是在父线程创建子线程时复制的,由于线程池的复用机制,“子线程”只会复制一次。要支持线程池中能访问提交任务线程的本地变量,其实只需要在父线程在向线程池提交任务时复制父线程的上下环境,那在子线程中就能够如愿访问到父线程中的本地遍历,实现本地环境变量在线程调用之中的透传,实现链路跟踪,这也就是TransmittableThreadLocal最本质的实现原理。

3.2.1 TransmittableThreadLocal类图

在这里插入图片描述

TransmittableThreadLocal继承自InheritableThreadLocal,接下来将从set方法为入口,开始探究TransmittableThreadLocal实现原理。

3.2.2 set方法详解

public final void set(T value) {

super.set(value); // @1

// may set null to remove value

if (null == value) // @2

removeValue();

else

addValue();

}

代码@1:首先调用父类的set方法,将value存入线程本地遍历,即Thread对象的inheritableThreadLocals中。

代码@2:如果value为空,则调用removeValue()否则调用addValue。

那接下来重点看看这两个方法有什么名堂:

private void addValue() {

if (!holder.get().containsKey(this)) { // @1

holder.get().put(this, null); // WeakHashMap supports null value.

}

}

private void removeValue() {

holder.get().remove(this);

}

代码@1:当前线程在调用threadLocal方法的set方法(即向线程本地遍历存储数据时),如果需要设置的值不为null,则调用addValue方法,将当前ThreadLocal存储到TransmittableThreadLocal的全局静态变量holder。holder的定义如下:

private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =

new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {

@Override

protected Map<TransmittableThreadLocal<?>, ?> initialValue() {

return new WeakHashMap<TransmittableThreadLocal<?>, Object>();

}

@Override

protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {

return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);

}

};

从中可以看出,使用了线程本地变量,内部存放的结构为Map<TransmittableThreadLocal<?>, ?>,即该对象缓存了线程执行过程中所有的TransmittableThreadLocal对象,并且其关联的值不为空。但这样做有什么用呢?

为了解开这个难题,可能需要大家对ttl这个框架的使用有一定的理解,本文由于篇幅的原因,将不会详细介绍,如有大家有兴趣,可以查阅其官网了解其使用:https://github.com/alibaba/transmittable-thread-local

ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4));

TransmittableThreadLocal parent = new TransmittableThreadLocal();

parent.set(“value-set-in-parent”);

Runnable task = new Task(“1”);

Callable call = new Call(“2”);

executorService.submit(task);

executorService.submit(call);

我们从submit为突破口,来尝试解开holder属性用途。

class ExecutorTtlWrapper implements Executor, TtlEnhanced {

private final Executor executor;

ExecutorTtlWrapper(@Nonnull Executor executor) {

this.executor = executor;

}

@Override

public void execute(@Nonnull Runnable command) {

executor.execute(TtlRunnable.get(command)); // @1

}

@Nonnull

public Executor unwrap() {

return executor;

}

}

在向线程池提交任务时,会使用TtlRunnable对提交任务进行包装。接下来将重点探讨TtlRunnable。

3.2.2 TtlRunnable详解

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

总结

以上是字节二面的一些问题,面完之后其实挺后悔的,没有提前把各个知识点都复习到位。现在重新好好复习手上的面试大全资料(含JAVA、MySQL、算法、Redis、JVM、架构、中间件、RabbitMQ、设计模式、Spring等),现在起闭关修炼半个月,争取早日上岸!!!

下面给大家分享下我的面试大全资料

  • 第一份是我的后端JAVA面试大全

image.png

后端JAVA面试大全

  • 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理

字节二面拜倒在“数据库”脚下,闭关修炼半个月,我还有机会吗?

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

  • 第三份是Spring全家桶资料

字节二面拜倒在“数据库”脚下,闭关修炼半个月,我还有机会吗?

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-VIpPX77b-1711099646209)]
[外链图片转存中…(img-yunr9JWQ-1711099646210)]
[外链图片转存中…(img-g2LVZNOY-1711099646210)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-kPJtWsNE-1711099646210)]

总结

以上是字节二面的一些问题,面完之后其实挺后悔的,没有提前把各个知识点都复习到位。现在重新好好复习手上的面试大全资料(含JAVA、MySQL、算法、Redis、JVM、架构、中间件、RabbitMQ、设计模式、Spring等),现在起闭关修炼半个月,争取早日上岸!!!

下面给大家分享下我的面试大全资料

  • 第一份是我的后端JAVA面试大全

[外链图片转存中…(img-i2LYJy5W-1711099646211)]

后端JAVA面试大全

  • 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理

[外链图片转存中…(img-fOkaXLcc-1711099646211)]

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

  • 第三份是Spring全家桶资料

[外链图片转存中…(img-XWGTjdjq-1711099646212)]

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值