ThreadLocal(使用场景,源码解析,面试题)

    科技2022-07-11  83

    文章目录

    一、什么是ThreadLocal?二、使用场景三、源码分析四、面试题1、ThreadLocal中的ThreadLocalMap中的key为什么要是弱引用?采用了弱引用还会发生内存泄露问题吗?2、ThreadLocal的原理是什么?怎样做到线程之间互不干扰的?3、ThreadLocalMap存放数据时hash值是用的Object.hashCode吗?为什么?4、为什么ThreadLocal要自定义一款Map而不用JDK中的HashMap?5、每个线程的ThreadLocalMap是什么时候创建的?6、ThreadLocalMap怎么解决hash冲突的?7、ThreadLocalMap的扩容阈值是多少?达到扩容阈值一定会扩容吗?扩容算法过程是啥?8、说一下ThreadLocalMap中的get流程9、探测式过期数据清理过程中遇到正常数据怎么处理?10、说一下ThreadLocalMap中的set流程,碰到过期数据怎么处理?

    一、什么是ThreadLocal?

    多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行访问的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal就提供了一种不需要使用同步措施(加锁等)就能保证线程安全的一种机制。

    ThreadLocal提供了线程内部存储变量的能力,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。ThreadLocal底层为每个线程都维护了一个ThreadLocalMap,key就是threadLocal对象,value就是需要存储的局部变量。

    二、使用场景

    1、JDBC连接数据库时,为每个线程分配一个连接Connection,这样可以保证每个线程都在各自的Connection上进行数据库的操作,不会出现A线程关闭了B线程正在使用的Connection。

    2、当前用户信息需要被线程内的所有方法共享,使用ThreadLocal实现不同方法间的资源共享。避免加锁产生的性能问题。

    3、spring中是用ThreadLocal解决线程安全问题,Spring框架并没有对单例进行任何多线程的封装处理,关于bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态,所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如View Model对象),就需要自行保证线程安全。将多态bean的作用域由“singleton”变更为“prototype”或者使用ThreadLocal进行处理。

    三、源码分析

    public class ThreadLocal<T> { //用来确定entry(key和value)存放到当前线程ThreadLocalMap中的哪个桶位?threadLocalHashCode &(table.length-1)。 private final int threadLocalHashCode = nextHashCode(); //创建ThreadLocal对象时会使用到,每创建一个threadLocal对象就会使用nextHashCode分配一个hash值给这个对象,会有规律的增长,保证值分配均匀 private static AtomicInteger nextHashCode = new AtomicInteger(); //创建一个threadLocal对象,这个ThreaLocal.nextHashCoe这个值就会增长0x61c88647, //这个值非常特殊,它是一个斐波那契数,也叫黄金分割数,Hash增量为这个数的好处就是hash分布非常均匀。 private static final int HASH_INCREMENT = 0x61c88647; //创建ThreadLocal对象时,会给当前对象分配一个hash值,就是用这个方法,每次都会在原来的基础上加一个黄金分割值,以保证元素的均匀分布。 private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } //默认返回null,一般情况下,咱们都是需要重写这个方法的,用来初始化当前线程的value protected T initialValue() { return null; } public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); } public ThreadLocal() { } //返回当前线程与当前ThreadLocal对象相关联的线程局部变量,这个变量是由当前线程能后访问。 public T get() { //获取当前线程对象的引用 Thread t = Thread.currentThread(); //获取到当前线程对象的threaLocals引用 ThreadLocalMap map = getMap(t); //条件成立:说明当前线程已经拥有自己的ThreadLocalMap对象了 if (map != null) { //通过当前threadLocal对象获取threadLocal中该thradLocal关联的entry。 ThreadLocalMap.Entry e = map.getEntry(this); //条件成立,说明当前线程已初始化过线程局部变量 if (e != null) { //初始化value值,是Object对象为空 T result = (T)e.value; //返回该局部变量value return result; } } //执行到这里有两种情况 //1.当前线程对应的threadLocalMap是空 //2.当前线程与当前threadlocal对象没有生成过相关联的线程局部变量。 //初始化当前线程对象的value,如果当前线程的threadLocalMap也没有初始化,还会初始化map。 return setInitialValue(); } private T setInitialValue() { //获取value,默认是null,大多数情况下会重写这个方法,设置value T value = initialValue(); //获取当前线程对象 Thread t = Thread.currentThread(); //获取当前线程内部的ThredLocalMap对象 ThreadLocalMap map = getMap(t); //条件成立,说明当前线程内部已经初始化过threadLocalMap了 if (map != null) //保存当前threadLocal与当前线程生成的线程局部变量 //key:当前threadLocal对象 value:当前线程与当前threadLocal相关的局部变量 map.set(this, value); else //说明当前线程内部未初始化threadLocalMap,调用创建方法进行创建 //参数1:当前线程 参数2:当前线程与threadLocal相关的局部变量 createMap(t, value); //返回当前线程与threadLocal对象相关联的局部变量 return value; } //修改当前线程与当前threadLocal对象相关联的线程局部变量。 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对象 createMap(t, value); } //移除当前线程与当前threadLocal对象相关联的线程局部变量 public void remove() { //获取当前线程的threadLocalMap对象 ThreadLocalMap m = getMap(Thread.currentThread()); //条件成立:说明当前线程已经初始化过threadLocalMap对象了 if (m != null) //调用threadLocalMap.remove(当前threadLocal对象) m.remove(this); } ThreadLocalMap getMap(Thread t) { //返回当前线程的threadLocals return t.threadLocals; } void createMap(Thread t, T firstValue) { //传递t的的意义就是要访问当前这个线程t.threadLocals字段,给这个字段初始化 //创建一个ThreadLocalMap对象,初始k-v为当前threadLocal对象,当前线程与当前threadLocal相关的局部变量。 t.threadLocals = new ThreadLocalMap(this, firstValue); } static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); } T childValue(T parentValue) { throw new UnsupportedOperationException(); } static final class SuppliedThreadLocal<T> extends ThreadLocal<T> { private final Supplier<? extends T> supplier; SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); } @Override protected T initialValue() { return supplier.get(); } } //key使用的是弱引用,保存的是threalLocal对象 //value使用的是强引用,保存的是当前线程的局部变量 static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //初始化散列表时数组的初始长度 private static final int INITIAL_CAPACITY = 16; //内部散列表的引用 private Entry[] table; //存放的entry的个数,表示当前散列表数组的占用情况 private int size = 0; //扩容触发的阈值,初始值是len*2/3 //触发后调用rehash()进行扩容,先做一次全量检查全局过期数据,把散列表中的所有过期的entry移除。 private int threshold; // Default to 0 //计算初始扩容阈值,当前数组的长度的2/3 private void setThreshold(int len) { threshold = len * 2 / 3; } //获取当前位置的下一个位置 private static int nextIndex(int i, int len) { //如果当前下标+1<len,返回加1后的值,否则返回0,实现了一种环绕式的访问策略 return ((i + 1 < len) ? i + 1 : 0); } //获取当前位置的前一个下标位置 private static int prevIndex(int i, int len) { //如果是第一个位置,就返回最后一个位置的下标,也是一种环绕的方法 return ((i - 1 >= 0) ? i - 1 : len - 1); } //创建threadLocalMap,只有在第一次初始value时才会创建 //firstKey:threadLocal对象 firstrValue:当前线程要存储的线程局部变量 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //创建entry数组长度为16,表示thradLocalMap内部的散列表 table = new Entry[INITIAL_CAPACITY]; //寻址算法:确定当前的firstKey和firstValue封装成的entry在entry数组中的存储位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //创建entry对象,放到指定位置 table[i] = new Entry(firstKey, firstValue); //因为这是创建threadLocalMap时第一个存入value值,所以size是1 size = 1; //设置扩容阈值,当前数组长度的2/3 setThreshold(INITIAL_CAPACITY); } private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { 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++; } } } } //ThreadLocal对象get操作实际上是由ThreadLocalMap.getEntry()代理完成的。 //key:某个ThreadLocal对象,因为散列表中存储的entry.key类型是ThreadLocal private Entry getEntry(ThreadLocal<?> key) { //路由出key所在的Entry数组中的下标 int i = key.threadLocalHashCode & (table.length - 1); //访问Entry数组中的指定位置的Entry元素 Entry e = table[i]; //条件成立:说明当前位置的Entry有执并且与当前查询的key一致 if (e != null && e.get() == key) //返回这个entry return e; else //这个方法会继续向当前桶位的后面继续搜索 //因为存储时发生hash冲突后,并没有在entry层面形成链表,它的处理是线性的向后找到一个可以使用的桶位,然后存放进去。 return getEntryAfterMiss(key, i, e); } //key:threadLocal对象 i:key计算出来的index(表示当前桶位不是你要找的)e:index出的Entry值 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { //获取当前threadLocalMap中的散列表 Entry[] tab = table; //entry数组的长度 int len = tab.length; //循环处理的当前元素 while (e != null) { //获取当前entry元素中的key ThreadLocal<?> k = e.get(); //条件成立:说明向后查询的过程中找到了需要的这个entry,返回就行 if (k == key) return e; //条件成立:说明当前桶位中的entry中的key的threadLocal对象已经被GC回收了,因为key是弱引用,key=e.get()=null if (k == null) //做一次探测式过期数据回收 expungeStaleEntry(i); else //更新index,继续向后搜索 i = nextIndex(i, len); //获取下一个index中的entry e = tab[i]; } return null; } //ThreaLocal使用set方法给当前线程添加threadLocal-value键值对 private void set(ThreadLocal<?> key, Object value) { //散列表的引用 Entry[] tab = table; //散列表数组长度 int len = tab.length; //计算key在散列表中对应的位置 int i = key.threadLocalHashCode & (len-1); //以当前key对应的位置,向后查询,找到可以使用的位置。 //什么时候slot可以使用呢? //1、k==key 说明key相等可以直接替换 //2、碰到一个过期的slot,这个时候可以强行占用 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //获取当前entry中的key ThreadLocal<?> k = e.get(); //条件成立,说明要添加的这个元素的key和当前key相等,是一个替换操作。 if (k == key) { //替换value e.value = value; return; } //条件成立,说明当前tab[i]是过期数据 if (k == null) { //替换过期数据 replaceStaleEntry(key, value, i); return; } } //执行到这里,说明for循环碰到了tab[i]=null的情况,说明添加的这个数据是新数据 //创建一个新的entry对象放到tab[i]中 tab[i] = new Entry(key, value); //数组中的元素个数加1 int sz = ++size; //条件1:!cleanSomeSlots(i, sz)成立,说明没有使用启发式清理清理到数据 //条件2:散列表中的元素个数大于扩容阈值 if (!cleanSomeSlots(i, sz) && sz >= threshold) //调用rehash方法 rehash(); } //清理方法 private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //遍历散列表,找到要清理的key这个entry for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //条件成立:当前entry就是要清理的元素 if (e.get() == key) { //当前元素的引用置为空 e.clear(); //以当前位置进行一次探测式清理 expungeStaleEntry(i); return; } } } //replaceStaleEntry 替换过期数据的方法 //key:threadLocal对象(为空,因为过期),value:当前线程与threadlocal关联的局部变量, //staleSlot:上层set方法传入的,就是过期数据在entry数组中的位置index private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { //散列表的引用 Entry[] tab = table; //散列表数组的长度 int len = tab.length; //散列表中元素的引用 Entry e; //用来记录找到的过期数据的下标,初始为staleSlot int slotToExpunge = staleSlot; //以staleSlot向前开始迭代查找,找有没有过期的数据,e==null时循环结束 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) //条件成立,说明找到了过期数据,将找到的过期数据的下标赋给sloToExpung slotToExpunge = i; //以当前staleSlot开始,向后迭代查找有没有过期数据,e==null时循环结束 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { //获取当前tab[i]中的key ThreadLocal<?> k = e.get(); //如果tab[i]的key与传入的key相同,需要进行替换操作 if (k == key) { //替换value e.value = value; //将tab[staleSlot]过期数据放到tab[i]的位置 tab[i] = tab[staleSlot]; //将value替换后的e赋给过期数据,完成了过期数据优化 tab[staleSlot] = e; //条件成立:一开始向前迭代查找过期数据时并未找到 if (slotToExpunge == staleSlot) //将过期数据的下标修改为当前循环的index slotToExpunge = i; //启发式清理 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } //条件1:key==null说明当前entry就是一个过期数据 //条件2:说明前驱扫描中未发现过期数据 if (k == null && slotToExpunge == staleSlot) //因为向后查找中找到了过期数据,就将slotToExpunge更新为当前位置 slotToExpunge = i; } //执行到这里,说明向前和向后迭代都未找到过期数据,过期数据就只staleSlot这一个entry //将这个过期数据的value置null,方便gc进行回收 tab[staleSlot].value = null; //然后创建一个新的entry放在这个过期数据的位置上 tab[staleSlot] = new Entry(key, value); //条件成立:说明不止staleSlot这一个过期数据存在,所以要开启数据清理逻辑 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } //探测式清理方法 //参数staleSlot,table[staleSslot]就是一个过期数据,以这个位置开始继续向后查找过期数据,直到碰到table[staleSslot].key=null结束 private int expungeStaleEntry(int staleSlot) { //散列表引用 Entry[] tab = table; //散列表当前长度 int len = tab.length; //当前桶位的value置为空,帮助gc tab[staleSlot].value = null; //entry也置为空, tab[staleSlot] = null; //因为少了一个元素,所以减1 size--; //表示当前遍历的entry节点 Entry e; //表示当前遍历的index int i; //从staleSlot+1的位置开始搜索过期数据,直到碰到entry数组中的entry为空 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { //获取当前遍历节点entry的key ThreadLocal<?> k = e.get(); //条件成立:说明k表示的threadlocal对象已经被GC回收了,当前entry属于脏数据了,需要置为空,让GC来进行垃圾回收 if (k == null) { //value,entry都置为空,让gc回收 e.value = null; tab[i] = null; size--; } else { //说明当前遍历的entry是非过期数据,因为前面有可能清理掉了几个过期数据,<这里做的就是优化这个未过期数据的位置> //且当前entry存储时有可能碰到hash冲突了,位置向后偏移了,这个时候应该去优化位置。尽量往正确位置靠拢(哈希冲突后的元素存储在一起) //这样的话,查询的时候效率会更高 //重新计算当前entry对应的index下标 int h = k.threadLocalHashCode & (len - 1); //说明当前entry存储时,就是发生过hash冲突,然后向后偏移过了。 if (h != i) { //将entry当前位置设置为null tab[i] = null; //以正确位置h开始,向后查找第一个可以存放entry的位置 while (tab[h] != null) h = nextIndex(h, len); //将当前元素放到正确位置 tab[h] = e; } } } return i; } //启发式清理方法 //i:表示探测式清理法在清理过期数据后返回的结果(一般是清理的最后一个过期数据的下标), //n:当前散列表的长度 private boolean cleanSomeSlots(int i, int n) { //启发式清理的状态,false表示没有使用启发式清理方法清理到数据 boolean removed = false; //散列表的引用 Entry[] tab = table; //散列表的长度 int len = tab.length; do { //探测式清理结果的下一个位置(因为探测式清理的结果表示这个i已经被清理过了),启发式清理的开始位置 i = nextIndex(i, len); //当前i位置的entry Entry e = tab[i]; //条件1:tab[i]为空 //条件2:i位置的entry里的key为空,是个过期数据 if (e != null && e.get() == null) { //对n进行重新赋值 n = len; //修改启发式清理的状态 removed = true; //使用探测式清理法以i为开始节点,做一次清理工作 i = expungeStaleEntry(i); } //假设table长度为16 //16>>>1 =8 //8>>>1 =4 } while ( (n >>>= 1) != 0); return removed; } private void rehash() { //使用探测式清理法再对全表进行一次过期数据清理,清理完成后散列表中不再有过期数据 expungeStaleEntries(); //当前数组中元素的个数大于等于扩容阈值的3/4时进行扩容 if (size >= threshold - threshold / 4) //扩容算法 resize(); } //扩容方法 private void resize() { //旧散列表的引用 Entry[] oldTab = table; //旧散列表的数组长度 int oldLen = oldTab.length; //新数组长度,为原来的2倍 int newLen = oldLen * 2; //创建新的散列表 Entry[] newTab = new Entry[newLen]; //新散列表中元素的个数 int count = 0; //数据迁移操作 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { //拿到当前entry的key ThreadLocal<?> k = e.get(); //条件成立说明当前数据是过期数据 if (k == null) { e.value = null; // Help the GC } else { //计算key在新数组中的位置 int h = k.threadLocalHashCode & (newLen - 1); //如果计算出来的这个位置已经有元素,就要找到一个里这个位置最近的位置 while (newTab[h] != null) //位置后移 h = nextIndex(h, newLen); //把该元素放到找到的这个位置中 newTab[h] = e; //元素数量加1 count++; } } } //设置新的扩容阈值(新数组长度的2/3) setThreshold(newLen); size = count; table = newTab; } //扩容时对全表进行探测式数据清理的方法 private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) expungeStaleEntry(j); } } } }

    四、面试题

    1、ThreadLocal中的ThreadLocalMap中的key为什么要是弱引用?采用了弱引用还会发生内存泄露问题吗?

    当我们在方法中创建一个threadLocal对象时,栈中就有一个强引用指向这个对象,而threadLocalMap中的key也是threadLocal对象,当调用set方法时,就会有threadLocalMap中的key的一个引用指向它,如果这个key是强引用,那么当栈帧中的强引用销毁了,因为threadLocalMap中还有这个对象的引用,这个对象不能被垃圾回收,会造成严重的内存泄露。

    注意:虽然是弱应用,保证了key指向的threadLocal对象能被及时回收,但是value对象还是需要threadLocalMap调用set或get方法才会去回收整个散列表,因此弱引用不能完全保证内存不泄露。

    解决办法:在不使用某个threadLocal对象时,要及时的手动调用remove方法来删除它,尤其是在线程池中,线程是重复使用的,要特别注意这个问题。

    2、ThreadLocal的原理是什么?怎样做到线程之间互不干扰的?

    ThreadLocal类里面有一个ThreadLocalMap的静态内部类,每个线程都有一个自己的ThreadLocalMap类型的引用threadLocals。

    ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

    ThreadLocalMap中维护的是一个Entry数组,每个entry有两个节点key和value,key表示threadLocal对象,它是一个弱引用。value是当前线程要保存的局部变量。当当前线程被销毁时,ThreadLocalMap也就会被垃圾回收。

    3、ThreadLocalMap存放数据时hash值是用的Object.hashCode吗?为什么?

    用的不是Object.hashCode,而是自己定义的获取hashCode的方法,并且它的初始值是一个黄金分割数,每次hash时都使用原子类在初始值上加上这个黄金分割数。

    private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }

    这样做的好处:使元素在散列表中的分布更加均匀,个人认为:也与它采用的是开发地址法来解决哈希冲突有关,为发生哈希冲突后的元素预留位置。

    4、为什么ThreadLocal要自定义一款Map而不用JDK中的HashMap?

    (1)ThreadLocalMap中的key是threadLocal类型,并且是弱引用。

    (2)ThreadLocalMap在写数据和查数据的过程中提供了清理过期数据的功能,从某种意义上来说,解决了内存泄漏的问题。

    5、每个线程的ThreadLocalMap是什么时候创建的?

    采用的是一种延迟初始化的思想,在当前线程第一个get或者set方法时才会进行判断,如果ThreaLocalMap为空,就会创建一个map。

    void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

    6、ThreadLocalMap怎么解决hash冲突的?

    采用的是开放地址法:当元素发生哈希冲突时,会线性向数组的后面查找,一直找到Entry为空的槽位停止寻找,将当前元素放入。当然还有其他情况,比如遇到过期数据的处理,后面会讲解。

    7、ThreadLocalMap的扩容阈值是多少?达到扩容阈值一定会扩容吗?扩容算法过程是啥?

    扩容阈值是数组长度的2/3,达到扩容阈值不一定会扩容,它会调用一个rehash方法,这个方法中会对全表进行一次扫描,进行探测式数据清理,清理过期数据,清理完之后,如果当前数组中的元素数量还是大于扩容阈值的3/4,才会真正触发扩容。

    private void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); }

    扩容的过程:

    创建一个新数组,是原数组的二倍,遍历原来的数组,如果当前元素不为空(空就不迁移),就获取它的key,如果key为null(过期数据不需要迁移),则把它的value赋为null,帮助GC;如果不为空,根据key计算index,判断是否发生哈希冲突,如果发生,线性向后查找直到找到合适位置,然后放入元素,如果没有冲突,直接放入元素。最后重新计算新的扩容阈值(数组长度的2/3)

    8、说一下ThreadLocalMap中的get流程

    首先会根据传入的key计算出一个index,比较index位置的key是否和传入的key相等,相等的话直接返回table[index],不相等需要向后遍历进行查找。以计算出的这个index处的元素开始,向后查找,如果遇到元素的key=null的情况,对着这个位置进行一次探测式过期数据清理(不光是清理数据的功能)。直到找到这个数据或者没找到返回null。 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) //清理方法还有调整正常数据的功能 expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }

    9、探测式过期数据清理过程中遇到正常数据怎么处理?

    首先会重新计算一个key的index位置,判断是不是等于当前这个位置,等于不做操作(写入时没有发生hash冲突),不等于说明发生过哈希冲突了,考虑到当前正在执行清理数据的逻辑,当前位置的前面可能有过期数据被干掉。所以这个正常的数据需要重新寻找一个更合适的位置去存放。这个位置理论上应该接近于正确的index。就是从这个位置开始向后遍历,找到一个槽位为空的位置,然后放入这个正常数据。以此来优化散列表的查询性能。 int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; }

    10、说一下ThreadLocalMap中的set流程,碰到过期数据怎么处理?

    首先使用key计算出index,以index开始遍历数组,直到遇到entry为null时,循环结束:

    遍历过程中key一致时,直接替换value。遇到过期数据时:看下面遇到entry为null时,直接创建一个新的entry,放入当前位置,然后判断是否需要进行rehash操作。

    set过程中遇到过期数据:采用的是一种替换过期数据的思想

    用slotExpunge表示向前遍历过程中数组中的第一个过期数据的下标,方便后面采用启发式清理方法进行数据清理

    以当前结点(过期的)开始向前去检查是否有过期key,如果有更新slotExpunge值,直到碰到null结束从当前结点向后查找是否存在key一致的情况 如果存在:替换value,并且和刚开始的过期数据进行位置交换,然后从slotExpunge开始向后检查清理过期数据(使用的是启发式清理)。 如果不存在:则创建一个新的entry,替换刚开始的过期数据,然后从slotExpunge向后进行数据清理。
    Processed: 0.011, SQL: 8