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

前言

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

ThreadLocal是什么?

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

阿里面试,死磕ThreadLocal源码,原来是这样回答的_第1张图片

应用场景

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

ThreadLocal实现原理

ThreadLocal数据结构

阿里面试,死磕ThreadLocal源码,原来是这样回答的_第2张图片

如上类图可知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()方法流程:

  1. 首先会调用getMap()方法获取到当前线程对象ThreadLocal引用
  2. 当ThreadLocal引用不为空时,则调用getEntry()方法获取到当前线程和当前ThreadLocal对象相关联的局部变量,若不为空,则返回
  3. 若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流程:

  1. 首先会调用getMap()方法获取到当前线程对象ThreadLocal引用
  2. 当ThreadLocal引用不为空时,则调用ThreadLocalMap类中set()进行数据覆盖
  3. 若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() 流程

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

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() 分析

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

rehash()分析

private void rehash() {
		// 清理掉当前散列表内的所有过期的数据
            expungeStaleEntries();

            // 当前散列表长度是否大于阈值*0.75
            if (size >= threshold - threshold / 4)
            // 扩容操作
                resize();
        }

rehash()方法 过程

  1. 首先清除当前散列表中所有过期数据
  2. 判断当前散列表长度是否大于阈值的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()方法 过程:

  1. 创建新的散列表,长度为旧散列表长度的两倍
  2. 遍历旧散列表,若value值不为空时,将重新计算Hash值,将其填充到新的散列表中
  3. 进行设置阈值 , 阈值默认为散列表长度的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()方法 过程

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

到这里,ThreadLocal核心源码已经分析完毕了,估计你现在已经炸裂了吧,心想,这里什么牛人可以这么设计出来,牛人就是牛人,不得不佩服
阿里面试,死磕ThreadLocal源码,原来是这样回答的_第3张图片

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

灵魂拷问

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

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

总结

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

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

阿里面试,死磕ThreadLocal源码,原来是这样回答的_第4张图片

你可能感兴趣的:(面试,源码,Java,面试,Java,ThreadLocal,JUC,源码)