从ThreadLocal谈到TransmittableThreadLocal,从使用到原理1

本文涉及的产品
可观测链路 OpenTelemetry 版,每月50GB免费额度
简介: 从ThreadLocal谈到TransmittableThreadLocal,从使用到原理

前言

谈到这个其实还是蛮有意思的,因为我最近有在看SpringCloud相关的有趣的知识点,在玩那个链路追踪(sleuth+zipkin)的时候,看博客看着看着,就变成了看关于怎么自己手动实现链路追踪的文章去啦

因为小小的好奇心吧,然后在掘金看到了这篇:自实现分布式链路追踪 方案&实践-作者:蝎子莱莱爱打怪

也就是这篇文章中提到了我今天想谈的这个知识点 TransmittableThreadLocal ,顺着文章提供的链接,我就去了github上溜达。

至此才有了我笔下的这篇东拼西凑的博文~ 虽然学过不少多线程的知识了,但我可以说这是我第一次接触TransmittableThreadLocal 吗 🤕 哈哈

希望能够有一些收获吧


在开始聊 TransmittableThreadLocal 之前,不可避免的还是要先说一说大家相对熟悉的 ThreadLocalInheritableThreadLocal 的。

知道痛点的由来,才能更清楚 TransmittableThreadLocal 的产生以及使用场景。

如果是已经了解过ThreadLocal和InheritableThreadLocal的朋友,可以直接点击TransmittableThreadLocal目录开始阅读。

image.png

ThreadLocal

ThreadLocal 相对来说,大伙应该都是非常熟悉的啦,不然你可能也不会点开这篇博客啦,哈哈

ThreadLocal直接翻译为线程本地(变量),我们经常会使用到它来保存一些线程隔离的全局的变量信息。使用ThreadLocal维护变量时,每个线程都会获得该线程独享一份变量副本。

ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,即变量在线程间隔离而在方法或类间共享的场景。 确切的来说,ThreadLocal 并不是专门为了解决多线程共享变量产生的并发问题而出来的,而是给提供了一个新的思路,曲线救国。

使用场景

简单说一下我看到过的~

  1. 保存用户的登录信息
    在没有使用权限框架的单体项目中,ThreadLocal 可能会用来临时保存请求时的用户信息。
  2. 链路追踪
    当前端发送请求到服务 A时,服务 A会生成一个类似UUIDtraceId字符串,将此字符串放入当前线程的ThreadLocal中,在调用服务 B的时候,将traceId写入到请求的Header中,服务 B在接收请求时会先判断请求的Header中是否有traceId,如果存在则写入自己线程的ThreadLocal中。

总的来说就是上下文信息的传递以及线程隔离的使用场景会比较适合。

注意:ThreadLocal保存的信息只能够在当前线程中可访问到,如果再开一个异步线程则无法进行访问,后续会说。

举个小例子

public class ThreadLocalDemo1 {
    private static ThreadLocal<String> userHolder = new ThreadLocal<>();
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 保存临时用户信息");
            String userInfo="宁在春";
            userHolder.set(userInfo);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 不会收到线程2的影响,因为ThreadLocal 线程本地存储
            System.out.println(Thread.currentThread().getName() + " 获取临时用户信息 " + userHolder.get());
            // 线程结束前,需要移除
            userHolder.remove();
        }, "myThread1").start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 保存临时用户信息");
            String userInfo="hello world";
            userHolder.set(userInfo);
            userHolder.remove();
        }, "myThread2").start();
    }
}
#输出
myThread1 保存临时用户信息
myThread2 保存临时用户信息
myThread1 获取临时用户信息 宁在春

从结果可以很明显的看出,线程之间的变量并不相互影响。

类图结构

image.png

图2:关键类图

Thread中有两个变量分别是ThreadLocal.ThreadLocalMap threadLocalsinheritableThreadLocalsinheritableThreadLocals后续再谈。

在这里我们可以知道的是每个线程都会有一个自己的 ThreadLocalMap,而ThreadLocalMap是ThreadLocal下的一个内部类.

ThreadLocalMap从命名上也可以看出来,它就是一个Map结构的对象(不过它不同于HashMap,它没有链表),ThreadLocalMap的key值是ThreadLocal,value则是我们要放入的值。

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

ThreadLocalMap中的内部Entry,就是用来保存键值对的,Entry 继承了 WeakReference(弱引用),为防止内存泄漏而设计的。

public class ThreadLocal<T>{
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

(Java引用相关的知识,大家需要去自己了解一下下)

怎么实现线程隔离的?

要说是怎么实现线程隔离的,其实就是在set()、get()方法的具体实现,我们set的值,为什么不会被其他的线程所读取。

set()方法

public void set(T value) {
    // 1、获取当前线程
    Thread t = Thread.currentThread();
    // 2、获取当前线程的threadlocals成员变量
    ThreadLocalMap map = getMap(t);
    // 3、判断map是否为null
    if (map != null)
        // 如果不为null,就直接将value放进map中
       // key是当前的threadLocal,value就是传进来的值
        map.set(this, value);
    else
        // 如果为 null,初始化一个map,再将value 放进map中
       // key是当前的threadLocal,value就是传进来的值
        createMap(t, value);
}

getMap()方法:返回当前线程的 threadLocals 变量

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

createMap()方法:进行 ThreadLocalMap 的初始化

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap的结构:ThreadLocalMap 是ThreadLocal下的一个内部类,ThreadLocalMap内还有一个Entry的内部类,并且继承了WeakReference,这里就是Java的弱引用,当堆空间不足时,会清理未被引用的entry。对了 ThreadLocalMap的key就是ThreadLocal,value就是我们想要保存的变量副本。

public class ThreadLocal<T>{
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

ThreadLocalMap的初始化方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 创建一个 Entry 数组
    table = new Entry[INITIAL_CAPACITY];
    // 计算hash值 这里的哈希冲突的解决办法采用了开放地址法,hash冲突的情况则下标挪一位再找
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 创建一个 Entry 放进Entry 数组
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //计算要调整大小的下一个大小值。
    setThreshold(INITIAL_CAPACITY);
}

小结:

每个对象最开始的 threadLocals 都为空,当线程调用 ThreadLocal.set() 或 ThreadLocal.get()时,就会调用到 createMap() 进行初始化。然后在当前线程里面,如果要使用副本变量,就可以通过 get() 在 threadLocals 里面查找。

image.png

图3:set()方法流程图

get()方法

java

复制代码

public T get() {
    // 获取到当前线程
    Thread t = Thread.currentThread();
   // 2、获取当前线程的threadlocals成员变量
    ThreadLocalMap map = getMap(t);
    //3、判断map是否为null
    if (map != null) 
        //3.1、如果不为null,根据当前的ThreadLocal 从当前线程中的ThreadLocals中取出map存储的变量副本
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果存储的值不为null,就返回值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //
    return setInitialValue();
}

map.getEntry()简单说一下:

  1. 计算hash值,取值
  2. 不为null且相等则直接返回
  3. 否则按照hash冲突的解决方式继续寻找,直至最后找到返回结果或者返回null。

setInitialValue()方法:没有找到则初始化返回一个null值

private T setInitialValue() {
    T value = initialValue();// 这里初始化的是一个 null 值
    Thread t = Thread.currentThread(); 
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
// ThreadLocal类 → initialValue()()方法
protected T initialValue() {
    return null;
}

小结

image.png

图3:get()方法流程图

小结

总结起来就是:在每条线程 Thread 内部都有一个 ThreadLocal.ThreadLocalMap 类型的成员变量 threadLocals,这个 threadLocals 就是用来存变量副本的,其中的 key 就为当前 ThreadLocal 对象,value 为我们存储的变量副本。

image.png

图4:内部结构

自始至终,这些本地变量都不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量,那个线程私有的threadLocalMap 里面

ThreadLocal就是一个工具壳和一个key,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。

从ThreadLocal谈到TransmittableThreadLocal,从使用到原理2:https://developer.aliyun.com/article/1394837

相关实践学习
基于OpenTelemetry构建全链路追踪与监控
本实验将带领您快速上手可观测链路OpenTelemetry版,包括部署并接入多语言应用、体验TraceId自动注入至日志以实现调用链与日志的关联查询、以及切换调用链透传协议以满足全链路打通的需求。
分布式链路追踪Skywalking
Skywalking是一个基于分布式跟踪的应用程序性能监控系统,用于从服务和云原生等基础设施中收集、分析、聚合以及可视化数据,提供了一种简便的方式来清晰地观测分布式系统,具有分布式追踪、性能指标分析、应用和服务依赖分析等功能。 分布式追踪系统发展很快,种类繁多,给我们带来很大的方便。但在数据采集过程中,有时需要侵入用户代码,并且不同系统的 API 并不兼容,这就导致了如果希望切换追踪系统,往往会带来较大改动。OpenTracing为了解决不同的分布式追踪系统 API 不兼容的问题,诞生了 OpenTracing 规范。OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。Skywalking基于OpenTracing规范开发,具有性能好,支持多语言探针,无侵入性等优势,可以帮助我们准确快速的定位到线上故障和性能瓶颈。 在本套课程中,我们将全面的讲解Skywalking相关的知识。从APM系统、分布式调用链等基础概念的学习加深对Skywalking的理解,从0开始搭建一套完整的Skywalking环境,学会对各类应用进行监控,学习Skywalking常用插件。Skywalking原理章节中,将会对Skywalking使用的agent探针技术进行深度剖析,除此之外还会对OpenTracing规范作整体上的介绍。通过对本套课程的学习,不止能学会如何使用Skywalking,还将对其底层原理和分布式架构有更深的理解。本课程由黑马程序员提供。
目录
相关文章
|
6月前
|
存储 安全 Java
玩转ThreadLocal:让你的代码更优雅的并发之道
玩转ThreadLocal:让你的代码更优雅的并发之道
61 0
|
6月前
|
存储 安全 Java
面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
字节面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
78 0
|
3月前
|
存储 设计模式 安全
深入理解ThreadLocal原理
本文深入探讨了Java中的ThreadLocal及其内部数据结构ThreadLocalMap的工作原理和特性,帮助读者理解如何利用ThreadLocal实现线程局部变量的隔离和线程安全。
深入理解ThreadLocal原理
|
5月前
|
存储 安全 Java
《ThreadLocal使用与学习总结:》史上最详细由浅入深解析ThreadLocal
《ThreadLocal使用与学习总结:》史上最详细由浅入深解析ThreadLocal
43 0
|
6月前
|
设计模式 缓存 Java
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理3
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理
850 1
|
6月前
|
存储 安全 Java
ThreadLocal原理讲解
ThreadLocal原理讲解
54 0
|
6月前
|
存储 Java
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理2
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理
596 0
|
存储 Java
大厂是怎么用ThreadLocal?ThreadLocal核心原理分析
ThreadLocal**是Java中的一个线程本地变量类。它可以让每个线程都有自己独立的变量副本,而不会相互影响。
117 1
|
Java 数据库连接
ThreadLocal原理和实践
ThreadLocal是线程本地变量,解决多线程环境下成员变量共享存在的问题。ThreadLocal为每个线程创建独立的的变量副本,他的特性是该变量的引用对全局可见,但是其值只对当前线程可用,每个线程都将自己的值保存到这个变量中而各线程不受影响。
160 0
ThreadLocal原理和实践
|
存储 设计模式 Java
ThreadLocal的短板,我 TransmittableThreadLocal 来补上!(上)
ThreadLocal的短板,我 TransmittableThreadLocal 来补上!(上)
ThreadLocal的短板,我 TransmittableThreadLocal 来补上!(上)