声明这里的是自己看博客的总结主要是应对面试自已如果面试官问,我会按照我总结的进行回答,需要详细看原理的可以参考我参考的博客(文章的最后面)
ThreadLocal
threaldLocal中的set流程(包含对过期key的处理)
- 首先计算该threadLocal这个key的哈希值这个哈希值是通过黄精分割数进行累加的目的是让threadLocalMap中key的分布更均匀减少哈希碰撞
- 算出哈希值后,通过哈希值和map长度-1进行安位与得到想要插入的具体桶下标
- 如果这个位置entry为空那么直接new一个entry设置进去直接返回
- 如果这个位置entry不为空
-
- 如果这个位置的key值与刚计算的哈希值相同那么执行置换操作,直接返回
- 如果这个entry不为空并且key为null说明这个位置是一个过期值,那下面一个执行相同操作
- 如果key不为null那么说明发生了哈希碰撞,那么会往后找位置,过程中遇到相同的key那么执行替换之后直接返回,如果不同继续往后遍历,遇到key为null执行下面一个操作,知道迭代到null位置停止
- 上面的下一个操作就是replaceStaleEntry(key, value, i);这个方法主要是两次遍历,
-
- 第一次遍历是从当前key为null的位置往前遍历找到map中第一个过期的位置指针1,迭代结束条件是访问到的位置是null
- 接着开始从
staleSlot
向后查找,也是碰到Entry
为null
的桶结束。
如果迭代过程中,碰到 k == key,这说明这里是替换逻辑,替换新数据并且交换当前staleSlot
位置。如果slotToExpunge == staleSlot
,这说明replaceStaleEntry()
一开始向前查找过期数据时并未找到过期的Entry
数据,接着向后查找过程中也未发现过期数据,修改开始探测式清理过期数据的下标为当前循环的 index,即slotToExpunge = i
。最后调用cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
进行启发式过期数据清理。 - 接着看看他是怎样先进性探测试清理的expungeStaleEntry
-
-
- 从过期的起点位置开始往后进行迭代,首先将当前位置slotToExpunge的过期位置进行清理(因为传进来的第一个位置一定是过期位置)
- 然后往后进行遍历迭代遇到entry为null结束条件
- 过程中如果遇到过期的元素那么就直接清理掉,(entry不为null,而key为null)
- 如果遇到正常的元素即key!=null,的entry那么会对这个key进行哈希重映射,即计算这个key的哈希值后重新到找到原本应该插入的桶的位置,如果还是发生哈希碰撞那么久线性往后移动找到空的位置插入就可以了,这个的目的是为了让元素回归原本正确的哈希槽位置(目的是增加访问效率)
- 探测清理完返回最后一个线性清理的最后一个元素位置
-
-
- 这里会基于这个i继续进行启发式清理,启发式清理传入两个参数cleanSomeSlots(int i, int n)
-
-
- 其迭代次数就是n每次减半知道减少到0为止
- 每次迭代就是从当前i出发寻找map中i往后下一个过期数据,将其坐标重新设置为i,然后对i这个位置执行一次探测试清理
-
-
- 最后再再原来执行replaceStaleEntry方法传入第一个过期位置,将要set的值放入这个entry里面然后就直接返回了
- 如果还继续往下执行那么就说明插入的位置是最后跌倒找的的null位置,即插入过程中没发现过期元素,那么到这里就直接新建一个新的entry将对应的key,value设置进去就好了,最后元素总个数size++
- 添加完成后那么就需要检查一下是否需要扩容对于threadLocal中的扩容是不同于HashMap的
-
- 首先他会基于当前位置i(这里就是最后插入元素的位置i,与之前过期清理无关的),先进行一次启发式清理传入参数(i,最新size个数),经过一次启发式清理后如果总的size还是大于扩容阈值(map长度的2/3)那么执行rehash操作
-
- 而rehash操作首先需要进行一次以map起点即下标为0为止开始进行一次完整的线性清理,这个清理会让所有过期key全部清理掉,如果发现现在的元素个数size还是大于了原来阈值的3/4那么才进行真正的扩容resize()操作
- resize()就是将map扩容为原来两倍然后将扩容阈值设置为新map长度的2/3
InheritableThreadLocal
作用,用于子线程共享父线程的变量信息
我们使用ThreadLocal
的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。
为了解决这个问题,JDK 中还有一个InheritableThreadLocal
类
基本使用
父子线程:创建子线程的线程是父线程,比如实例中的 main 线程就是父线
ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("父线程设置的值");
new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start();
}
// 子线程输出:父线程设置的值
实现原理
InheritableThreadLocal 源码:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 1、获取父线程的数据
protected T childValue(T parentValue) {
return parentValue;
}
// 2、 获取 inheritableThreadLocals 变量
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
// 3、为当前线程进行 inheritableThreadLocals 的初始化
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
在这个方法里面,只有T childValue()
是我们在 ThreadLocal 中没有接触过的方法,那么肯定是有点妙用的。其他的比如createMap()方法就是从threadLocals
改成了inheritableThreadLocals
,没有太多改变。
真正的起点是在new Thread(() - >{})
这段代码中,相信很多人,包括我在此之前都没有怎么看过Thread的构造函数过程
实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc,
// 该参数默认是 true
boolean inheritThreadLocals) {
// ...
//这里的currentThread()其实就是创建当前线程的那个线程,而当我们再在主线程new InheritableThreadLocal()
//时就会将t.InheritableThreadLocal进行创建threadlLocalMap而不是初始化ThreadLocal属性
Thread parent = currentThread();
// 判断父线程(创建子线程的线程)的 inheritableThreadLocals 属性不为 null
我们需要关注的点
// 判断 父线程的inheritThreadLocals 和 当前线程的 inheritThreadLocals 是否为 null
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
// 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// ..
}
// 【本质上还是创建 ThreadLocalMap,只是把父类中的可继承数据设置进去了】
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
// 获取父线程的哈希表
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 【逐个复制父线程 ThreadLocalMap 中的数据】
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 调用的是 InheritableThreadLocal#childValue(T parentValue)
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++;
}
}
}
}
小结
所以InheritableThreadLocal本质上就是通过复制来实现父子线程之间的传值。
在this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
完这一段代码后,子线程就已经存储了父线程的所有Entry信息了。
局限性:
InheritableThreadLocal
支持子线程访问父线程,本质上就是在创建线程的时候将父线程中的本地变量值全部复制到子线程中。
但是在谈到并发时,不可避免的会谈到线程池,因为线程的频繁创建和销毁,对于程序来说,代价实在太大。
而在线程池中,线程是复用的,并不用每次新建,那么此时InheritableThreadLocal
复制的父线程就变成了第一个执行任务的线程了,即后面所有新建的线程,他们所访问的本地变量都源于第一个执行任务的线程(期间也可能会遭遇到其他线程的修改),从而造成本地变量混乱。
比如:
假如我们有这样的一个流程,10个请求到达controller,然后调用service,在service中我们还要执行一个异步任务,最后等待结果的返回。
10个service - > 10个异步任务 ,在service,我们会设置一个变量副本,在执行异步任务的子线程中,需要get出来进行调用。
public class InheritableThreadLocalDemo3 {
/**
* 业务线程池,service 中执行异步任务的线程池
*/
private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);
/**
* 线程上下文环境,在service中设置环境变量,
* 然后在这里提交一个异步任务,模拟在子线程(执行异步任务的线程)中,是否可以访问到刚设置的环境变量值。
*/
private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
// 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
for (int i = 0; i < 10; i++) {
// 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
new Thread(new ServiceThread(i)).start();
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//关闭线程池
businessExecutors.shutdown();
}
/**
* 模拟Service业务代码
*/
static class ServiceThread implements Runnable {
private int i;
public ServiceThread(int i) {
this.i = i;
}
@Override
public void run() {
requestIdThreadLocal.set(i);
System.out.println("执行service方法==>在"+Thread.currentThread().getName() + "中存储变量副本==>" + i);
// 异步编程 CompletableFuture.runAsync()创建无返回值的简单异步任务,businessExecutors 表示线程池~
CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
try {
// 模拟执行时间
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:"+requestIdThreadLocal.get());
}, businessExecutors);
requestIdThreadLocal.remove();
}
}
}
运行结果
执行service方法==>在Thread-0中存储变量副本==>0
执行service方法==>在Thread-1中存储变量副本==>1
执行service方法==>在Thread-6中存储变量副本==>6
执行service方法==>在Thread-3中存储变量副本==>3
执行service方法==>在Thread-4中存储变量副本==>4
执行service方法==>在Thread-5中存储变量副本==>5
执行service方法==>在Thread-2中存储变量副本==>2
执行service方法==>在Thread-7中存储变量副本==>7
执行service方法==>在Thread-8中存储变量副本==>8
执行service方法==>在Thread-9中存储变量副本==>9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:1
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:7
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:2
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:5
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:7
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:2
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:5
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:1
发现问题:
可以看到在子线程中获取到的变量值已经重复~ 此时线程变量副本值已经错乱啦。这是因为,我们
使用的是线程池作为子线程,其实我们实际过程中想要的是每个父线程对应有一个子线程共享,理想结果是父线程打印多少子线程就打印多少,出现错乱的原因是,线程池的线程复用,比如第一个线程使用完后他并不是将线程进行销毁而是继续复用,继续服用就代表,还是复用该线程之前创建的threadlocalmap那么就会出现错乱现象
## 原理小结(!!!!)
inheritableThreadLocl其实就是
- 首先在创建的时候即new InheritableThreaedLocal的时候就是先获取到当前的线程然后将当前线程的inheritableThreadLocal=new ThreadLocalMap,
- 然后其他自线程在创建线程的时候
-
- 首先获取到创建线程的当前线程,当前线程就是新线程的父线程,然后创建是检测到父线程的inheritableThreadLocal不为空,那么这个新的线程中的threadLocalMap就会基于当前线程即父线程的ThreadLocalMap去进行浅拷贝(拷贝每个threadLocalMap中entry的地址),这样就实现了子线程能够复用父线程的线程变量值
Ai回答:
- 创建
InheritableThreadLocal
实例:当创建InheritableThreadLocal
实例时,它本身并不直接存储任何值。它作为一个“键”,用于在后续操作中从当前线程的inheritableThreadLocals
中检索或设置值。 - 设置值:当调用
InheritableThreadLocal
实例的set
方法时,实际上是将值设置到当前线程的inheritableThreadLocals
中。如果当前线程的inheritableThreadLocals
尚未初始化,则会先初始化它。 - 线程继承:当创建一个新线程时,如果父线程的
inheritableThreadLocals
不为空,则新线程的inheritableThreadLocals
会被初始化为父线程inheritableThreadLocals
的一个浅拷贝。这意味着新线程将能够访问父线程中通过InheritableThreadLocal
设置的所有值,但这些值在新线程中是独立的副本,对它们的修改不会影响父线程或其他线程中的值。 - 获取值:当在新线程中调用
InheritableThreadLocal
实例的get
方法时,它会从当前线程的inheritableThreadLocals
中检索值。如果找到了相应的值,则返回它;如果没有找到,则返回null
(或者如果InheritableThreadLocal
被配置为具有初始值,则返回该初始值)。
InheritableThreadLocal参考文章:从ThreadLocal谈到TransmittableThreadLocal,从使用到原理2-阿里云开发者社区
目录