ThreadLocal源码分析和面试题难点

ThreadLocal是个啥?

threadlocal是一个本地的线程变量,解决了多线程之间的数据隔离。在Thread类中有一个threadLocals 字段, 它用来保存该线程本地变量,这样不同的线程就都有自己的threadLocals ,也就是说每个线程拥有自己独立的数据,保证了数据安全。

以前老的JDK版本中在ThreadLocal中维护一个大的map,所有的线程都保存到这个map中,体量比较大。在JDK8中,每个线程维护自己的数据,线程销毁,对应的map会被GC回收。

在ThreadLocalMap中有一个内部类Entry,继承WeakReference,在构造方法中传入的是ThreadLocal k, 这个k是一个弱引用,这有什么好处?

当threadLocal对象失去强引用且对象GC回收后,散列表中的与 threadLocal对象相关联的 entry#key ,再次去key.get() 时,拿到的是null。站在map角度就可以区分出哪些entry是过期的,哪些entry是非过期的。

解释一下这几个类的关系:

Thread 类:

public class Thread implements Runnable {
    /**其他。。。。。**/
    
    //保留本地线程的相关变量
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    // ThreadLocal的内核本质
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

ThreadLocal类:

/**
*连接ThreadLocalMap和Thread。
*来处理Thread的TheadLocalMap属性,包括init初始化属性赋值、get对应的变量,set设置变量等。
*通过当前线程,获取线程上的ThreadLocalMap属性,对数据进行get、set等操作。
*/
public class ThreadLocal<T> {
    
    /**
    *ThreadLocal的内部类,是一个定制的自定义哈希表,只适合用于维护线程对应ThreadLocal的值
    *ThreadLocalMap是ThreadLocal内部类,外部无法直接访问,允许在 Thread 类中以字段的形式声明
    *以助于处理存储量大,生命周期长的使用用途
    *此类定制的哈希表实体键值对使用弱引用WeakReferences 作为key
    */
    static class ThreadLocalMap {
        
    }
    
}

ThreadLocal的成员变量

//线程获取threadLocal.get()时 如果是第一次在某个 threadLocal对象上get时,会给当前线程分配一个value
//这个value和当前的threadLocal对象 被包装成为一个 entry 
//其中 key是 threadLocal对象,value是threadLocal对象给当前线程生成的value

//这个entry存放到 当前线程 threadLocals 这个map的哪个桶位? 
//与当前 threadLocal对象的threadLocalHashCode 有关系。
//使用 threadLocalHashCode & (table.length - 1) 的到的位置 就是当前entry需要存放的位置。
private final int threadLocalHashCode = nextHashCode();

/**
* 创建ThreadLocal对象时 会使用到,每创建一个threadLocal对象 就会使用nextHashCode 分配一个hash值给这个对象。
*/
private static AtomicInteger nextHashCode = new AtomicInteger();

/**
* 每创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647 。
* 这个数是黄金分割数。hash增量为这个数字,带来的好处就是 hash分布非常均匀。
*/
private static final int HASH_INCREMENT = 0x61c88647;

ThreadLocal源码分析

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);
}

get方法

/*
* 返回当前线程与当前ThreadLocal对象相关联的 线程局部变量,这个变量只有当前线程能访问到。
* 如果当前线程 没有分配,则给当前线程去分配(使用initialValue方法)    
*/
public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取到当前线程Thread对象的 threadLocals map引用
    ThreadLocalMap map = getMap(t);
    //条件成立:说明当前线程已经拥有自己的 ThreadLocalMap 对象了
    if (map != null) {
        //key:当前threadLocal对象
        //调用map.getEntry() 方法 获取threadLocalMap 中该threadLocal关联的 entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        //条件成立:说明当前线程 初始化过 与当前threadLocal对象相关联的 线程局部变量
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            //返回value..
            return result;
        }
    }

    //执行到这里有几种情况?
    //1.当前线程对应的threadLocalMap是空
    //2.当前线程与当前threadLocal对象没有生成过相关联的 线程局部变量..

    //setInitialValue方法初始化当前线程与当前threadLocal对象 相关联的value。
    //且 当前线程如果没有threadLocalMap的话,还会初始化创建map。
    return setInitialValue();
}

setInitialValue方法

/**
* setInitialValue方法初始化当前线程与当前threadLocal对象相关联的value。
* 且当前线程如果没有threadLocalMap的话,还会初始化创建map。
*/
private T setInitialValue() {
    //调用的当前ThreadLocal对象的initialValue方法,这个方法大部分情况下咱们都会重写。
    //value 就是当前ThreadLocal对象与当前线程相关联的 线程局部变量。
    T value = initialValue();
    //获取当前线程对象
    Thread t = Thread.currentThread();
    //获取当前线程内部的threadLocals threadLocalMap对象。
    ThreadLocalMap map = getMap(t);
    //条件成立:说明当前线程内部已经初始化过threadLocalMap对象了。
    //线程的threadLocals 只会初始化一次。
    if (map != null)
        //保存当前threadLocal与当前线程生成的 线程局部变量。
        //key: 当前threadLocal对象
        //value:线程与当前threadLocal相关的局部变量
        map.set(this, value);
    else
        //执行到这里,说明 当前线程内部还未初始化 threadLocalMap ,这里调用createMap 给当前线程创建map

        //参数1:当前线程   参数2:线程与当前threadLocal相关的局部变量
        createMap(t, value);

    //返回线程与当前threadLocal相关的局部变量
    return value;
}
//initialValue一般自己实现
private static final ThreadLocal<Integer> threadId =
    new ThreadLocal<Integer>() {
    @Override protected Integer initialValue() {
        return nextId.getAndIncrement();
    }
};

remove方法

public void remove() {
    //获取当前线程的 threadLocalMap对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    //条件成立:说明当前线程已经初始化过 threadLocalMap对象了
    if (m != null)
        //调用threadLocalMap.remove( key = 当前threadLocal)
        m.remove(this);
}

Thread最重要的就是set和get方法,这样看起来好像没什么高深啊!其实这只是表象,也就是它封装了,还记得ThreadLocalMap吗?

其实它才是ThreadLocal的内核方法,不然包装成内部类干啥呢?

接下来才是硬核!!!

ThreadLocalMap源码分析

我们先看一下ThreadLocalMap中都有些什么东西?

Entry内部类和成员变量

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

//初始化当前map内部 散列表数组的初始长度 16
private static final int INITIAL_CAPACITY = 16;

//threadLocalMap 内部散列表数组引用,数组的长度 必须是 2的次方数
private Entry[] table;

//当前散列表数组 占用情况,存放多少个entry。
private int size = 0;

/**
* 扩容触发阈值,初始值为: len * 2/3
* 触发后调用 rehash() 方法。
* rehash() 方法先做一次全量检查全局 过期数据,把散列表中所有过期的entry移除。
* 如果移除之后 当前 散列表中的entry 个数仍然达到  threshold - threshold/4  就进行扩容。
*/
private int threshold; // Default to 0

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

//下一个下标,环绕式访问
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

//上一个下标,环绕式访问
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

// 因为Thread.threadLocals字段是延迟初始化的,
// 只有线程第一次存储 threadLocal-value 时 才会创建 threadLocalMap对象。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    //寻址算法:key.threadLocalHashCode & (table.length - 1)
    //table数组的长度一定是 2 的次方数。
    //2的次方数-1 有什么特征呢?  转化为2进制后都是1.    16==> 1 0000 - 1 => 1111
    //1111 与任何数值进行&运算后 得到的数值 一定是 <= 1111
    //i 计算出来的结果 一定是 [0,table.length-1]
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

getEntry方法

//ThreadLocal对象 get() 操作 实际上是由 ThreadLocalMap.getEntry() 代理完成的。
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //条件一:成立 说明slot有值
    //条件二:成立 说明 entry#key 与当前查询的key一致,返回当前entry 给上层就可以了。
    if (e != null && e.get() == key)
        return e;
    else
        //有几种情况会执行到这里? 1.e == null  2.e.key != key

        //getEntryAfterMiss方法 会继续向当前桶位后面继续搜索 e.key == key 的entry.

        //为什么这样做呢??
        //因为存储时发生hash冲突后,并没有在entry层面形成链表.. 
        //存储时的处理 就是线性的向后找到一个可以使用的slot,并且存放进去。
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    
    //条件:e != null 说明 向后查找的范围是有限的,碰到 slot == null 的情况,搜索结束。
    //e:循环处理的当前元素
    while (e != null) {
        //获取当前slot 中entry对象的key
        ThreadLocal<?> k = e.get();
        //条件成立:说明向后查询过程中找到合适的entry了,返回entry就ok了。
        if (k == key)
            return e;
        //条件成立:说明当前slot中的entry#key 关联的 ThreadLocal对象已经被GC回收了.. 
        //因为key 是弱引用, key = e.get() == null.
        if (k == null)
            //"探测式"过期数据回收
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        //获取下一个slot中的entry。
        e = tab[i];
    }
    //执行到这里,说明关联区段内都没找到相应数据。
    return null;
}

set方法

private void set(ThreadLocal<?> key, Object value) {
    //获取散列表
    Entry[] tab = table;
    //获取散列表数组长度
    int len = tab.length;
    //计算当前key 在 散列表中的对应的位置
    int i = key.threadLocalHashCode & (len-1);

    //以当前key对应的slot位置 向后查询,找到可以使用的slot。
    //什么slot可以使用呢??
    //1.k == key 说明是替换
    //2.碰到一个过期的 slot ,这个时候 咱们可以强行占用呗。
    //3.查找过程中 碰到 slot == null 了。
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {

        //获取当前元素key
        ThreadLocal<?> k = e.get();

        //条件成立:说明当前set操作是一个替换操作。
        if (k == key) {
            //做替换逻辑。
            e.value = value;
            return;
        }

        //条件成立:说明 向下寻找过程中 碰到entry#key == null 的情况了,说明当前entry 是过期数据。
        if (k == null) {
            //碰到一个过期的 slot ,这个时候 咱们可以强行占用呗。
            //替换过期数据的逻辑。
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //执行到这里,说明for循环碰到了 slot == null 的情况。
    //在合适的slot中 创建一个新的entry对象。
    tab[i] = new Entry(key, value);
    //因为是新添加 所以++size.
    int sz = ++size;

    //做一次启发式清理
    //条件一:!cleanSomeSlots(i, sz) 成立,说明启发式清理工作 未清理到任何数据..
    //条件二:sz >= threshold 成立,说明当前table内的entry已经达到扩容阈值了..会触发rehash操作。
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

replaceStaleEntry方法

/* 
key: 键 threadLocal对象
value: val
staleSlot: 上层方法 set方法,迭代查找时 发现的当前这个slot是一个过期的 entry。
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
    //获取散列表
    Entry[] tab = table;
    //获取散列表数组长度
    int len = tab.length;
    //临时变量
    Entry e;

    //表示 开始探测式清理过期数据的 开始下标。默认从当前 staleSlot开始。
    int slotToExpunge = staleSlot;


    //以当前staleSlot开始 向前迭代查找,找有没有过期的数据。for循环一直到碰到null结束。
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len)){
        //条件成立:说明向前找到了过期数据,更新 探测清理过期数据的开始下标为 i
        if (e.get() == null){
            slotToExpunge = i;
        }
    }

    //以当前staleSlot向后去查找,直到碰到null为止。
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        //获取当前元素 key
        ThreadLocal<?> k = e.get();

        //条件成立:说明咱们是一个 替换逻辑。
        if (k == key) {
            //替换新数据。
            e.value = value;

            //交换位置的逻辑..
            //将table[staleSlot]这个过期数据 放到 当前循环到的 table[i] 这个位置。
            tab[i] = tab[staleSlot];
            //将tab[staleSlot] 中保存为 当前entry。 这样的话,咱们这个数据位置就被优化了..
            tab[staleSlot] = e;

            //条件成立:
            // 1.说明replaceStaleEntry 一开始时 的向前查找过期数据 并未找到过期的entry.
            // 2.向后检查过程中也未发现过期数据..
            if (slotToExpunge == staleSlot)
                //开始探测式清理过期数据的下标 修改为 当前循环的index。
                slotToExpunge = i;


            //cleanSomeSlots :启发式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        //条件1:k == null 成立,说明当前遍历的entry是一个过期数据..
        //条件2:slotToExpunge == staleSlot 成立,一开始时 的向前查找过期数据 并未找到过期的entry.
        if (k == null && slotToExpunge == staleSlot)
            //因为向后查询过程中查找到一个过期数据了,更新slotToExpunge 为 当前位置。
            //前提条件是 前驱扫描时 未发现 过期数据..
            slotToExpunge = i;
    }

    //什么时候执行到这里呢?
    //向后查找过程中 并未发现 k == key 的entry,说明当前set操作 是一个添加逻辑..

    //直接将新数据添加到 table[staleSlot] 对应的slot中。
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);


    //条件成立:除了当前staleSlot 以外 ,还发现其它的过期slot了.. 所以要开启 清理数据的逻辑..
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

在这里插入图片描述

expungeStaleEntry探索式清除

//参数 staleSlot   table[staleSlot] 就是一个过期数据,
//以这个位置开始 继续向后查找过期数据,直到碰到 slot == null 的情况结束。
private int expungeStaleEntry(int staleSlot) {
    //获取散列表
    Entry[] tab = table;
    //获取散列表当前长度
    int len = tab.length;

    // expunge entry at staleSlot
    //help gc
    tab[staleSlot].value = null;
    //因为staleSlot位置的entry 是过期的 这里直接置为Null
    tab[staleSlot] = null;
    //因为上面干掉一个元素,所以 -1.
    size--;

    // Rehash until we encounter null
    //e:表示当前遍历节点
    Entry e;
    //i:表示当前遍历的index
    int i;

    //for循环从 staleSlot + 1的位置开始搜索过期数据,直到碰到 slot == null 结束。
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        //进入到for循环里面 当前entry一定不为null


        //获取当前遍历节点 entry 的key.
        ThreadLocal<?> k = e.get();

        //条件成立:说明k表示的threadLocal对象 已经被GC回收了... 当前entry属于脏数据了...
        if (k == null) {
            //help gc
            e.value = null;
            //脏数据对应的slot置为null
            tab[i] = null;
            //因为上面干掉一个元素,所以 -1.
            size--;
        } else {
            //执行到这里,说明当前遍历的slot中对应的entry 是非过期数据
            //因为前面有可能清理掉了几个过期数据。
            //且当前entry 存储时有可能碰到hash冲突了,往后偏移存储了,这个时候 应该去优化位置,让这个位置更靠近 正确位置。
            //这样的话,查询的时候 效率才会更高!

            //重新计算当前entry对应的 index
            int h = k.threadLocalHashCode & (len - 1);
            //条件成立:说明当前entry存储时 就是发生过hash冲突,然后向后偏移过了...
            if (h != i) {
                //将entry当前位置 设置为null
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.


                //h 是正确位置。

                //以正确位置h 开始,向后查找第一个 可以存放entry的位置。
                while (tab[h] != null)
                    h = nextIndex(h, len);

                //将当前元素放入到 距离正确位置 更近的位置(有可能就是正确位置)。
                tab[h] = e;
            }
        }
    }
    return i;
}

在这里插入图片描述

cleanSomeSlots启发式清除

private boolean cleanSomeSlots(int i, int n) {
    //表示启发式清理工作 是否清楚过过期数据
    boolean removed = false;
    //获取当前map的散列表引用
    Entry[] tab = table;
    //获取当前散列表数组长度
    int len = tab.length;

    do {
        //这里为什么不是从i就检查呢?
        //因为cleanSomeSlots(i = expungeStaleEntry(???), n)  expungeStaleEntry(???) 返回值一定是null。

        //获取当前i的下一个 下标
        i = nextIndex(i, len);
        //获取table中当前下标为i的元素
        Entry e = tab[i];
        //条件一:e != null 成立
        //条件二:e.get() == null 成立,说明当前slot中保存的entry 是一个过期的数据..
        if (e != null && e.get() == null) {
            //重新更新n为 table数组长度
            n = len;
            //表示清理过数据.
            removed = true;
            //以当前过期的slot为开始节点 做一次 探测式清理工作
            i = expungeStaleEntry(i);
        }


        // 假设table长度为16
        // 16 >>> 1 ==> 8
        // 8 >>> 1 ==> 4
        // 4 >>> 1 ==> 2
        // 2 >>> 1 ==> 1
        // 1 >>> 1 ==> 0
    } while ( (n >>>= 1) != 0);

    return removed;
}

在这里插入图片描述

rehash方法

private void rehash() {
    //这个方法执行完后,当前散列表内的所有过期的数据,都会被干掉。
    expungeStaleEntries();


    // Use lower threshold for doubling to avoid hysteresis
    //条件成立:说明清理完 过期数据后,当前散列表内的entry数量仍然达到了 threshold * 3/4,真正触发 扩容!
    if (size >= threshold - threshold / 4)
        //扩容。
        resize();
}

resize方法

private void resize() {
    //获取当前散列表
    Entry[] oldTab = table;
    //获取当前散列表长度
    int oldLen = oldTab.length;
    //计算出扩容后的表大小  oldLen * 2
    int newLen = oldLen * 2;
    //创建一个新的散列表
    Entry[] newTab = new Entry[newLen];
    //表示新table中的entry数量。
    int count = 0;

    //遍历老表 迁移数据到新表。
    for (int j = 0; j < oldLen; ++j) {
        //访问老表的指定位置的slot
        Entry e = oldTab[j];
        //条件成立:说明老表中的指定位置 有数据
        if (e != null) {
            //获取entry#key
            ThreadLocal<?> k = e.get();
            //条件成立:说明老表中的当前位置的entry 是一个过期数据..
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                //执行到这里,说明老表的当前位置的元素是非过期数据 正常数据,需要迁移到扩容后的新表。。

                //计算出当前entry在扩容后的新表的 存储位置。
                int h = k.threadLocalHashCode & (newLen - 1);
                //while循环 就是拿到一个距离h最近的一个可以使用的slot。
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);

                //将数据存放到 新表的 合适的slot中。
                newTab[h] = e;
                //数量+1
                count++;
            }
        }
    }


    //设置下一次触发扩容的指标。
    setThreshold(newLen);
    size = count;
    //将扩容后的新表 的引用保存到 threadLocalMap 对象的 table这里。。
    table = newTab;
}

面试题

你对threadLocal的理解?

是一个本地线程副本变量工具类,主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,适用于各个线程不共享变量值的操作。

ThreadLocal 工作原理是什么?

每个线程的内部都维护了一个 ThreadLocalMap,它是一个 (key,value)数据格式,key 是一个弱引用,也就是 ThreadLocal 本身,而 value 存的是线程变量的值。

ThreadLocal 如何解决 Hash 冲突?

与 HashMap 不同,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则去判断下个位置,直至找到能够存放的位置。

ThreadLocal 的内存泄露是怎么回事?

ThreadLocalMap和Thread的生命周期是一样的,并且ThreadLocalMap使用ThreadLocal的弱引用作为key,如果threadlocal对象不存在外部的强引用的话,他就会被GC回收,这样就导致ThreadLocalMap中的key为null,而value还存在着强引用,只有当前线程退出后才会将value的强引用链释放掉,但是thread如果迟迟不释放,这个value的强引用链就一直存在,这就造成了内存泄漏。

当然jdk8中对其做了优化,key为null的情况下,ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

所以ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

正确使用方式:每次使用完ThreadLocal都调用它的remove()方法清除数据,或者将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

为什么 ThreadLocalMap 的 key 是弱引用?

Thread的生命周期和ThreadLocalMap是一样长的,若果key是强引用,会导致一个问题?引用的threadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,没收手动删除的话,会导致threadlocal不会被回收,导致内存泄漏问题。

如果key使用弱引用呢?这样引用的threadLocal的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value在下一次调用set、get、remove方法的时候会被清除,这算是一种优化方式吧!

ThreadLocalMap为什么结构不同于HashMap?

threadLocalMap的key是一个弱引用,而hashmap的key是一个强引用。这是为了防止内存泄漏,在写数据、查数据的过程中,他会及时清理过期数据,解决内存泄漏问题。value是强引用,时间长不释放,会有问题。

ThreadLocalMap的创建时间?

他是一个加载机制,只有在第一次set、get的时候会创建,并且map是独一份的,它会去判断当前线程是否已经绑定了map,绑定了就直接使用,否则再创建。

ThreadLocalMap的内部结构是什么样的?

初始化大小默认是16,也就是table.length == 16,这个长度必须是2的n次幂,这样有什么好处呢?就是在我们计算下标的时候,使用的是hash &(table.length - 1),2的n次幂-1,二进制低位就是1111,这样与hash作按位与得到的下标一定是[0,table.length-1],这就相当关于对length取余,便于寻址,效率高。

讲一下ThreadLocalMap中的get的流程

首先根据传来的key,计算出下标i,然后去判断table[i]是否为空。如果不为空并且传来的key和查询得到的key一样,则说明找到了,返回即可。如果为空,或者key与查询得到的Entry的key不一样,就向后继续遍历查找,知道查找的slot为null为止。在后续查找中,找到Entry的key与传来的key一致,返回即可,没找到返回null。这里面有一个情况,就是当查找的Entry的key为null,说明这是一个过期数据,它所关联的threadlocal对象已经被GC了,这里需要做一次探索式过期数据回收。

讲一下探索式过期数据回收是什么过程

在查询的时候,table[i]的keynull, 说明这是一个过期数据,它所关联的threadlocal对象已经被GC了,需要把这个位置的数据干掉。也就是将table[i].value=null,table[i]=null,这样就不会造成内存泄漏了。然他还会向后搜寻,一直到slotnull。碰到过期数据,继续清除,碰到正常数据,他会rehash正常数据的一个位置,将其放到一个距离正确位置 更近的位置。

讲一下ThreadLocalMap中的set的流程

首先根据key计算出位置i,然后得到table[i],他是一个Entry,然后得到Entry的key,如果两个一样说明是一个替换操作,替换返回即可。如果Entry的key是null,则说明他是一个过期数据,可以它强行占用。都不是的话,向后面继续查找,如果找到一个slot为null,则要新创建一个Entry来保存threadlocal对象,最后收尾的时候会做一次启发式清理的工作。

详细说一下启发式清理

其实也是清除过期数据用的,里面有一个dowhile循环,遍历table得到一个Entry,判断是不是过期数据,不是就继续向后,并且搜索范围改为原来的一半。如果是过期数据,则进行一次探索式过期数据回收,并更新搜索范围为table.length。

扩容相关?

扩容是在set方法中的一个操作,添加完元素后,如果超过了扩容阈值,也就是table长度的2/3,首先他会去做一个rehash,他会执行一次过期数据清理工作,结束后散列表内的entry数量仍然达到了 threshold * 3/4,则进行真正的扩容。

扩容后的散列表是老表长度的2倍,然后遍历老表将数据迁移过来。

迁移过程中,碰到Entry的key是null,则说明key所关联的Threadlocal对象已经被GC,这时将Entry的value设置为null,help gc。

对于正常数据来说,根据Entry的k的hashcode & (新的数组长度-1)得到储存的位置放置。

最后就count++,设置新的阈值,将newTable的引用赋给table,结束!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TigRer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值