从ThreadLocal到InheritableThreadLocal再到TransmittableThreadLocal,从简单的使用,到明白个别知识原理。
前言
谈到这个其实还是蛮有意思的,因为我最近有在看SpringCloud相关的有趣的知识点,在玩那个链路追踪(sleuth+zipkin)的时候,看博客看着看着,就变成了看关于怎么自己手动实现链路追踪的文章去啦
文章中提到了我今天想谈的这个知识点 TransmittableThreadLocal
,顺着文章提供的链接,我就去了github上溜达。
至此才有了我笔下的这篇东拼西凑的博文~
在开始聊 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);
}
复制代码
小结:
每个