全链路跟踪之线程上下文Thread Local实战(完整源码)

写在开头:
我是「猿码天地」,一个热爱技术、热爱编程的IT猿。技术是开源的,知识是共享的!
写博客是对自己学习的总结和记录,如果您对Java、分布式、微服务、中间件、Spring Boot、Spring Cloud等技术感兴趣,可以关注我的动态,我们一起学习,一起成长!
用知识改变命运,让家人过上更好的生活,互联网人一家亲!
关注微信公众号【猿码天地】,获取更多干货技能,一起吃肉喝汤,陪你一起撸代码!

一、背景
ThreadLocal是JDK默认提供的本地线程变量,用来存储在整个调用链中都需要访问的数据,并且是线程安全的。由于本文的写作背景是笔者需要在公司落地全链路跟踪平台,一个基本并核心的功能需求是用户的每个操作动作需要在整个调用链中进行埋点传递,线程上下文环境成为解决这个问题最合适的技术。

二、ThreadLocal解决什么问题?
在这里插入图片描述
ThreadLocal是在Thread类之外实现的一个功能(java.lang.ThreadLocal), 但它会为每个线程分别存储一份唯一的数据。正如它的名字所说的,它为线程提供了本地存储,也就是说你所创建出来变量对每个线程实例来说都是唯一的。和线程 名,线程优先级类似,你可以自定义出一些属性,就好像它们是存储在Thread线程内部一样。

就以数据库事务这一常用场景来举例说明,比如每个线程需要访问数据库,就需要获取数据库的连接Connection对象,在实际中,我们会用数据库连接池来重复利用Connection,首先线程池,这里是一个共享变量,线程池的实现必须保证多个线程同时从线程池中获取Connection不会重复,然后每个线程使用单独的Connection,并且该Connection被一个线程占用后,其他线程压根就不会使用到,也不会试图去使用一个已经被其他线程占用的Connection对象。由于一个线程在执行过程中,可能需要多次操作数据库,所以我们的设计就是一个线程在执行过程中,只与一个Connection打交道,也就是整个线程的执行过程(执行环境)需要保存刚获取的Connection,最简单有效的办法,就是把这个Connection保存在线程对象的某个属性中,ThreadLocal就是干这事的。ThreadLocal并不是为这个Connection复制一份,多个线程都使用这个副本,不是这样的,一个Connection对象在任意时刻,没有被复制多份。

核心意思:ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

我的观点:ThreadLocal是线程一个本地变量,是线程的执行上下文。

三、ThreadLocal原理
ThreadLocal相关的类的类图结构
在这里插入图片描述
如上类图可知Thread类中有一个threadLocals和inheritableThreadLocals都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap,默认每个线程中这两个变量都为null,只有当前线程第一次调用了ThreadLocal的set或者get方法时候才会进行创建。其实每个线程的本地变量不是存放到ThreadLocal实例里面的,而是存放到调用线程的threadLocals变量里面。也就是说ThreadLocal类型的本地变量是存放到具体的线程内存空间的。

ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面存放起来,当调用线程调用它的get方法时候再从当前线程的threadLocals变量里面拿出来使用。如果调用线程一直不终止那么这个本地变量会一直存放到调用线程的threadLocals变量里面,所以当不需要使用本地变量时候可以通过调用ThreadLocal变量的remove方法从当前线程的threadLocals里面删除该本地变量。另外Thread里面的threadLocals为何设计为map结构呢?很明显是因为每个线程里面可以关联多个ThreadLocal变量。

四、源码分析
ThreadLocal对外提供的API如下:
public T get()
从线程上下文环境中获取设置的值。
public void set(T value)
将值存储到线程上下文环境中,供后续使用。
public void remove()
清除线程本地上下文环境。

源码分析get

public T get() {
   
   Thread t = Thread.currentThread();  // @1
   ThreadLocalMap map = getMap(t);  // @2
   if (map != null) {
                                   // @3
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
   
         @SuppressWarnings("unchecked")
         T result = (T)e.value;
         return result;
      }
   }
   return setInitialValue();  // @4
}

代码@1:获取当前线程。
代码@2:获取线程的threadLocals属性。
代码@3:如果线程对象的threadLocals属性不为空,则从该Map结构中,用threadLocal对象为键去查找值,如果能找到,则返回其value值,否则执行代码@4。
代码@4:如果线程对象的threadLocals属性为空,或未从threadLocals中找到对应的键值对,则调用该方法执行初始化。

ThreadLocal#setInitialValue

private T setInitialValue() {
   
   T value = initialValue();    // @1
   Thread t = Thread.currentThread();    // @2
   ThreadLocalMap map = getMap(t);    // @3
   if (map != null)           //@4
      map.set(this, value);
   else
      createMap(t, value);     // @5
   return value;
}

代码@1:调用initialValue()获取默认初始化值,该方法默认返回null,子类可以重写,实现线程本地变量的初始化。
代码@2:获取当前线程。
代码@3:获取该线程对象的threadLocals属性。
代码@4:如果不为空,则将threadLocal:value存入线程对象的threadLocals属性中。
代码@5:否则初始化线程对象的threadLocals,然后将threadLocal:value键值对存入线程对象的threadLocals属性中。

源码分析set

public void set(T value) {
   
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
      map.set(this, value);
   else
      createMap(t, value);
}

在掌握了get方法实现细节,set方法、remove其实现的逻辑基本一样,就是对线程对象的threadLocals属性进行操作(Map结构)。

五、适用场景
ThreadLocal 适用于如下两种场景:

  • 每个线程需要有自己单独的实例
  • 实例需要在多个方法中共享,但不希望被多线程共享

对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

六、全链路跟踪之线程上下文Thread Local实战

0、自定义注解

1)请求URL方法名称
2)操作名称
3)前端所属菜单
4)请求行为描述

package cn.wonhigh.shop.ms.infrastructure.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 链路追踪0、自定义注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({
   ElementType.METHOD})
public @interface RequestMonitor {
   

    /**
     * 请求URL方法名称
     * @return
     */
    String urlMethodName();

    /**
     * 操作名称
     * @return
     */
    String opType();

    /**
     * 前端所属菜单
     * @return
     */
    String menuName() default "";

    /**
     * 请求行为描述<br></br>
     * 使用占位符 ${} 替换变量
     * @return
     */
    String behaveMark() default "";

}

0、定义全局上下文工具类(用于储存线程上下文的值)

1)首先定义一个 私有、静态 ConcurrentHashMap
2)给线程上下文赋值,KEY为线程ID
3)根据当前线程ID获取线程上下文存储的值
4)根据当前线程ID获取链路追踪日志对象值
5)清空key为当前线程ID的值,防止内存泄漏

/**
 * 链路追踪0、全局上下文工具类,用于储存线程上下文的值
 */
public class ThreadLocalContext {
   

   /**
    * 定义Map
    */
    private static ConcurrentHashMap<Long, ThreadLocal<ShopNbMonitorLog>> currentThreadLocals = new ConcurrentHashMap<>();

    private ThreadLocalContext(){
   }

   public final static ThreadLocalContext INSTANCE = new ThreadLocalContext();

   /**
    * 给线程上下文赋值,KEY为线程ID
    * @param threadId
    * @param threadLocal
    */
   public void put(Long threadId, ThreadLocal<ShopNbMonitorLog> threadLocal){
   
        if(threadId != null && threadLocal != null) {
   
           //全链路跟踪日志记录对象
            ShopNbMonitorLog ShopNbMonitorLog = new ShopNbMonitorLog();
            //链路追踪ID
            String trackingId = UUID.randomUUID().toString().replaceAll("-","");
            ShopNbMonitorLog.setTrackingId(trackingId);
            threadLocal.set(ShopNbMonitorLog);
            currentThreadLocals.put(threadId, threadLocal);
        }
    }

   /**
    * 根据当前线程ID获取线程上下文存储的值
    * @param threadId
    * @return
    */
   public ThreadLocal<ShopNbMonitorLog> get(Long threadId){
   
        return currentThreadLocals.get(threadId);
    }

   /**
    * 根据当前线程ID获取链路追踪日志对象值
    * @param threadId
    * @return
    */
   public ShopNbMonitorLog getThreadLocalValue(Long threadId){
   
        return currentThreadLocals.get(threadId).get();
    }

   /**
    * 清空key为当前线程ID的值
    * @param threadId
    */
   public void remove(Long threadId){
   
        currentThreadLocals.remove(threadId);
    }

}

0、全链路跟踪日志记录操作类型

package cn.wonhigh.shop
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿码天地

相互学习,谢谢您的打赏。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值