前言
谈到这个其实还是蛮有意思的,因为我最近有在看SpringCloud相关的有趣的知识点,在玩那个链路追踪(sleuth+zipkin)的时候,看博客看着看着,就变成了看关于怎么自己手动实现链路追踪的文章去啦
因为小小的好奇心吧,然后在掘金看到了这篇:自实现分布式链路追踪 方案&实践-作者:蝎子莱莱爱打怪
也就是这篇文章中提到了我今天想谈的这个知识点 TransmittableThreadLocal
,顺着文章提供的链接,我就去了github上溜达。
至此才有了我笔下的这篇东拼西凑的博文~ 虽然学过不少多线程的知识了,但我可以说这是我第一次接触TransmittableThreadLocal
吗 🤕 哈哈
希望能够有一些收获吧
在开始聊 TransmittableThreadLocal
之前,不可避免的还是要先说一说大家相对熟悉的 ThreadLocal
和 InheritableThreadLocal
的。
知道痛点的由来,才能更清楚
TransmittableThreadLocal
的产生以及使用场景。
如果是已经了解过ThreadLocal和InheritableThreadLocal的朋友,可以直接点击TransmittableThreadLocal目录开始阅读。
ThreadLocal
ThreadLocal
相对来说,大伙应该都是非常熟悉的啦,不然你可能也不会点开这篇博客啦,哈哈
ThreadLocal
直接翻译为线程本地(变量)
,我们经常会使用到它来保存一些线程隔离的、全局的变量信息。使用ThreadLocal维护变量时,每个线程都会获得该线程独享一份变量副本。
ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,即变量在线程间隔离而在方法或类间共享的场景。 确切的来说,ThreadLocal 并不是专门为了解决多线程共享变量产生的并发问题而出来的,而是给提供了一个新的思路,曲线救国。
使用场景
简单说一下我看到过的~
- 保存用户的登录信息
在没有使用权限框架的单体项目中,ThreadLocal 可能会用来临时保存请求时的用户信息。 - 链路追踪
当前端发送请求到服务 A时,服务 A会生成一个类似UUID
的traceId
字符串,将此字符串放入当前线程的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 获取临时用户信息 宁在春
从结果可以很明显的看出,线程之间的变量并不相互影响。
类图结构
图2:关键类图
Thread中有两个变量分别是ThreadLocal.ThreadLocalMap threadLocals
和inheritableThreadLocals
,inheritableThreadLocals
后续再谈。
在这里我们可以知道的是每个线程都会有一个自己的 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 里面查找。
图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()简单说一下:
- 计算hash值,取值
- 不为null且相等则直接返回
- 否则按照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; }
小结
图3:get()方法流程图
小结
总结起来就是:在每条线程 Thread 内部都有一个 ThreadLocal.ThreadLocalMap
类型的成员变量 threadLocals
,这个 threadLocals 就是用来存变量副本的,其中的 key 就为当前 ThreadLocal
对象,value 为我们存储的变量副本。
图4:内部结构
自始至终,这些本地变量都不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量,那个线程私有的threadLocalMap 里面。
ThreadLocal就是一个工具壳和一个key,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理2:https://developer.aliyun.com/article/1394837