阿里面试,死磕ThreadLocal源码,原来是这样回答的

    技术2024-10-12  56

    前言

    我朋友cute轩前几天面试,正好阿里爸爸看他读过JUC包下源码,直接提起面试官小哥哥的兴趣,直接死磕ThreadLocal源码,面完试已经汗流浃背了,犹如一场高手对决,辛亏他看完源码,才勉强应战。今天就好好分析ThreadLocal源码。

    ThreadLocal是什么?

    ThreadLocal是JUC包下提供的,它提供了本地变量,也就是让每个线程都有自己的独立空间来存储变量,且该变量不会受到其他线程的影响,也可以理解为每个线程都可以在自己的独立空间中操作变量,不会影响到其他线程中的变量值。

    应用场景

    当某些数据是以线程为作用域且不同的线程中变量副本不同,就考虑使用Threadlocal

    ThreadLocal实现原理

    ThreadLocal数据结构

    如上类图可知Thread类中有一个threadLocals和inheritableThreadLocals都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap,默认每个线程中这个两个变量都为null,只有当前线程第一次调用了ThreadLocal的set或者get方法时候才会进行创建。

    ThreadLocalMap采用延迟加载策略,至少存放一个k-v值才会创建

    ThreadLocalMap基本组成

    // threadLocalMap 内部散列表数组引用,数组的长度 必须是 2的次方数 private Entry[] table; static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

    ThreadLocalMap内部采用Entry对象封装键值对,并且将key值设置为弱引用,便于其回收 弱引用: 使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收

    get()分析

    public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取到当前线程Thread对象的 threadLocals map引用 ThreadLocalMap map = getMap(t); // 代表当前线程 拥有自己的threadLocal map 对象 if (map != null) { // 获取 threadLocalMap 中 该threadLocal关联的 entry ThreadLocalMap.Entry e = map.getEntry(this); // 说明当前线程 初始化过 与当前threadLocal对象相关联的 线程局部变量 if (e != null) { @SuppressWarnings("unchecked") T result = (T) e.value; return result; } } // 执行到这里有几种情况? // 1.当前线程对应的threadLocalMap是空 // 2.当前线程与当前threadLocal对象没有生成过相关联的 线程局部变量.. // setInitialValue方法初始化当前线程与当前threadLocal对象 相关联的value // 且 当前线程如果没有threadLocalMap的话 还会初始化创建map return setInitialValue(); }

    get()方法流程:

    首先会调用getMap()方法获取到当前线程对象ThreadLocal引用当ThreadLocal引用不为空时,则调用getEntry()方法获取到当前线程和当前ThreadLocal对象相关联的局部变量,若不为空,则返回若ThreadLocal引用为空或者调用getEntry()方法返回空值时,则需要调用setInitialValue()方法处理

    setInitialValue()分析

    private T setInitialValue() { // 重写 T value = initialValue(); Thread t = Thread.currentThread(); // 获取当前线程和threadLocal相关联的 threadLocalMap 对象 ThreadLocalMap map = getMap(t); if (map != null) //保存当前threadLocal与当前线程生成的 线程局部变量。 //key: 当前threadLocal对象 value:线程与当前threadLocal相关的局部变量 map.set(this, value); else //执行到这里,说明 当前线程内部还未初始化 threadLocalMap, 这里调用createMap 给当前线程创建map //参数1:当前线程 参数2:线程与当前threadLocal相关的局部变量 createMap(t, value); return value; } // 创建ThreadLocalMap对象 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

    setInitialValue流程:

    首先会调用getMap()方法获取到当前线程对象ThreadLocal引用当ThreadLocal引用不为空时,则调用ThreadLocalMap类中set()进行数据覆盖若ThreadLocal引用为空的话,则会调用createMap()方法进行创建ThreadLocal对象

    ThreadLocalMap.set()方法分析

    private void set(ThreadLocal<?> key, Object value) { // 初始化数组 Entry[] tab = table; // 数组长度 int len = tab.length; // 计算桶位 int i = key.threadLocalHashCode & (len-1); // 从当前桶位开始遍历,找到可以可利用的桶位 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 获取value值 ThreadLocal<?> k = e.get(); // 条件成立 说明当前set操作是一个替换嘈杂哦 if (k == key) { e.value = value; return; } // 如果为空,表示该slot已被回收,需要进行探测式回收 if (k == null) { // 探测式回收 replaceStaleEntry(key, value, i); return; } } // 找到可利用的桶位 赋值 tab[i] = new Entry(key, value); int sz = ++size; // 检查是否需要进行扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

    ThreadLocalMap.set() 流程

    通过路由寻址算法计算出桶位slot,然后从该桶位进行遍历,若在遍历过程中,遇到key值相等的情况下,则说明此次过程是替换过程;若key为空,表示该key值已被GC回收,则需要执行探测式替换逻辑;否则走步骤二走到这步,说明已经找到限制的桶位,将k-v封装成Entry对象填充到该桶位中最后需要检测是否需要扩容

    ThreadLocalMap.replaceStaleEntry()分析

    // slot 指当前位置为空的桶位 private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; // 从当前的桶为上一个位置开始遍历,因为当前桶位数据为空 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // 说明找到比当前桶位更靠前的桶位值 用slotToExpunge记录 // Find either the key or trailing null slot of run, whichever // occurs first // 从从前桶位向后遍历 直到碰到null为止 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // //条件成立:说明咱们是一个 替换逻辑 if (k == key) { e.value = value; // 进行数据替换 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; // 进行启发式清理 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // 条件成立 说明向前遍历未找到过期的桶位,将当前桶位赋值到slotToExpunge if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // 执行到这里 说明当前set操作 是一个添加逻辑.. // 将数据添加到当前staleSlot中 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); //条件成立:除了当前staleSlot 以外 ,还发现其它的过期slot了.. 所以要开启 清理数据的逻辑.. if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }

    ThreadLocalMap.replaceStaleEntry() 分析

    从当前桶位的前一个桶位为起点,向前遍历直到遇到当前桶位为null,用slotToExpunge记录当前桶位从当前桶位的下一个桶位为起点,向后遍历,若在遍历过程中发现key值相等,则进行数据替换逻辑,如果slotToExpunge值不等于slot值的话,需要进行启发式清理,进行清理过期数据若条件二不成立的话,说明当前set操作是添加操作,在slot中添加数据,如果slotToExpunge值不等于slot值的话,需要进行启发式清理,进行清理过期数据

    rehash()分析

    private void rehash() { // 清理掉当前散列表内的所有过期的数据 expungeStaleEntries(); // 当前散列表长度是否大于阈值*0.75 if (size >= threshold - threshold / 4) // 扩容操作 resize(); }

    rehash()方法 过程

    首先清除当前散列表中所有过期数据判断当前散列表长度是否大于阈值的0.75倍,若大于,才进行扩容处理

    resize()分析

    private void resize() { // 旧数组 Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; // 新数组 Entry[] newTab = new Entry[newLen]; int count = 0; // 循环遍历进行数据迁移 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; // 条件成立 说明当前桶位值未被GC回收 if (e != null) { // 获取value值 ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { // 重新计算在新散列表中的桶位 int h = k.threadLocalHashCode & (newLen - 1); // 解决Hash冲突 向后遍历 直到遇到空桶位 while (newTab[h] != null) h = nextIndex(h, newLen); // 进行赋值 newTab[h] = e; count++; } } } // 设置阈值 = 散列表长度的2/3 setThreshold(newLen); size = count; table = newTab; }

    resize()方法 过程:

    创建新的散列表,长度为旧散列表长度的两倍遍历旧散列表,若value值不为空时,将重新计算Hash值,将其填充到新的散列表中进行设置阈值 , 阈值默认为散列表长度的2/3

    ThreadLocal#set()分析

    public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //获取当前线程的threadLocalMap对象 ThreadLocalMap map = getMap(t); //条件成立:说明当前线程的threadLocalMap已经初始化过了 if (map != null) //调用threadLocalMap.set方法 进行重写 或者 添加。 map.set(this, value); else //执行到这里,说明当前线程还未创建 threadLocalMap对象。 //参数1:当前线程 参数2:线程与当前threadLocal相关的局部变量 createMap(t, value); }

    ThreadLocal#set()方法 过程

    调用getMapp方法获取当前线程对应的ThreadLocalMap对象,若ThreadLocalMap对象为空,则调用ThreadLocalMap#set方法进行添加k-v键值对,该方法上面有具体流程;否则的话,执行步骤二调用createMap方法进行创建ThreadLocalMap对象

    到这里,ThreadLocal核心源码已经分析完毕了,估计你现在已经炸裂了吧,心想,这里什么牛人可以这么设计出来,牛人就是牛人,不得不佩服

    既然大家看见这里,说明已经把前面的核心理念基本掌握,那么我在这里出几道ThreadLocal面试题,再让大家印象深刻点。。。

    灵魂拷问

    ThreadLocalMap的Hash算法?ThreadLocalMap中Hash冲突如何解决?ThreadLocalMap扩容机制?ThreadLocalMap中过期key的清理机制?探测式清理和启发式清理流程?ThreadLocalMap.set()方法实现原理?ThreadLocalMap.get()方法实现原理?

    大家可以把答案写到评论中,欢迎讨论,答案下期给出,请期待

    总结

    我是一个每天都在闲聊编程的迈莫

    文章持续更新,可以微信搜索「 迈莫coding 」阅读,回复【888】有我准备的一线大厂面试资料

    Processed: 0.012, SQL: 9