不断的学习,我们才能不断的前进
一个好的程序员是那种过单行线马路都要往两边看的人

ThreadLocal

数据结构

ThreadLocal 是线程变量,要在<>里面指定变量的类型,作为ThreadLocalMap的value类型,ThreadLocalMap的key 是ThreadLocal类型,其中key是弱引用,value是强引用,所以每次使用完毕之后要使用remove,手动清除。
ThreadLocalMap 是ThreadLocal的静态内部类,在Thread 类里面有一个字段threadlocals,指向ThreadLocal.ThreadLocalMap。 所以每一个线程都有一个ThreadLocalMap,Thread.threadLocals获取这个map,每个线程都有自己的Thread类。

ThreadLocalMap采用的是懒初始化,只有在第一次set、get的时候才去初始化,get的时候存入的变量是null。

public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();
    private final int threadLocalHashCode = nextHashCode();
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    // key 是ThreadLocal 类型,value是任意类型
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
    
    public T get() {
        // Thread 里面有一个 threadlocals字段,这个字段的类型是ThreadLocal.ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
}

对 ThreadLocal的理解?

ThreadLocal叫做线程变量,也就是ThreadLocal中的变量是属于当前线程的,该变量属于线程私有。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,多个线程之间互不干扰,通过setInitialValue来进行赋值,只有在第一次访问get方法的时候才会给线程赋值

ThreadLocal的原理是什么呢?

在jdk1.8 里面,每一个Thread线程类里面都有一个threadLocals字段,这个字段指向堆里面的一个ThreadLocalMap,ThreadLocalMap存储的是当前线程与ThreadLocal对象相关联的数据。

ThreadLocalMap内存储的是什么?

ThreadLocalMap存储的是当前线程跟ThreadLocal对象相关联的数据。

ThreadLocal它是怎样做到线程之间互不干扰的呢?

每个线程拥有一个自己的ThreadLocalMap存储数据,线程访问某一个ThreadLocal对象get方法的时候会检测当前线程的map里面是否有key为ThreadLocal对象的Entry数据,如果没有,ThreadLocal的initalValue方法会创建一个Entry,然后存放到这个ThreadLocalMap里面。

老版本JDK的ThreadLocal是怎么设计的呢?

在ThreadLocal中维护一个大map,所有的线程的变量都会维护在一个map里面。

JDK8 版本的ThreadLocal设计有什么优势相比更早之前的老版本?

1.8之前会维护一个大的map,如果线程多的话这个map会很大,不利于维护,而jdk1.8 每个线程都有自己的map,生命周期跟线程一样,当线程被销毁的时候,线程对应的ThreadLocalMap就会在下一次GC被回收。

ThreadLocalMap 存放数据时,数据的hash值是从Object.hashCode()拿到的,还是其它方式?为什么?

ThreadLocal作为 key 存放在ThreadLocalMap里面,ThreadLocal重写了hashcode方法

为什么ThreadLocal选择自定义一款Map而没有沿用JDK中的HashMap?

ThreadLocal里面的map,key可以定义为自己想要的类型,它这个key使用的是弱引用类型,而HashMap的key使用的是强引用类型,而引用不会影响对象被回收的,强引用是永不被回收
ThreadLocalMap在写数据和查数据的过程中,有这个清理过期数据的功能,能够清除掉发现的过期数据,在一定程度上解决类内存泄露的问题,

每个线程的 ThreadLocalMap对象 是什么时候创建的呢?

是懒加载的机制,即在第一次get/set的时候,会检查是否已经绑定ThreadLocalMap对象,没有则创建。在线程的生命周期里面ThreadLocalMap只会被创建一次。

ThreadLocalMap 底层存储数据的数组长度 初始化是多少?

默认长度是16

这个数组大小为什么必须为 2的次方数?

跟HashMap是一样的,方便hash寻址。因为2的幂次方减1的二进制数,低位全是1。

ThreadLocalMap的扩容阈值是多少呢?

扩容的阈值是当前数组的2/3

ThreadLocalMap达到扩容阈值一定会扩容么?

如果达到扩容阈值的时候,会全量扫描一次整个hash表,然后清理掉过期的数据,如果清理掉过期数据后,还是达到扩容的2/3阈值,则进行扩容

扩容算法 你简单说一说

创建一个新的数组,长度为当前数组的两倍;然后遍历旧的数组,把其中的数据重新按照hash算法放入新的数组里面,更新完后,重新修改ThreadLocalMap的引用指向新的数组。扩容后会重新计算下次扩容的阈值

ThreadLocalMap对象的 get逻辑,你说下。

get传入的对象是当前的ThreadLocal对象,根据ThreadLocal对象的hash值跟数组长度减1 按位与后获得数组的下标,该位置可能是查找的元素也可能不是。如果是的话直接返回;
如果不是的话,说明发生了hash冲突,需要线性查找的方式去找到合适的位置,然后向后遍历,检测查找的数据是否等于要查找的对象,如果找到相等key则退出。
并且在查找过程还会检测数据是否过期,如果遇到过期数据,则将key==null的Entry置为null并且从过期位置的的下一个位置开始向下遍历直到遇到null,如果是过期数据则删除,如果是正常数据则判断重新hash的index等不等于当前索引,不等于就表示发生了hash冲突,需要重新hash,因为前面有过期数据被删除

过期数据就是指ThreadLocalMap中的key被GC回收量,因为是判断弱引用,每次GC都会回收,所以相关联的数据就没用了。

假设get首次未命中,向下迭代查找时,碰到过期数据了,怎么处理?

在查找过程还会检测数据是否过期,如果遇到过期数据,则将key==null的Entry置为null。并且从过期位置的的下一个位置开始向下遍历直到遇到null,如果是过期数据则删除,如果是正常数据则判断重新hash的index等不等于当前索引,不等于就表示发生了hash冲突,需要重新hash,因为前面有过期数据被删除。

探测式清理过期数据,向下迭代过程中碰到正常数据,怎么处理?

遇到正常数据,则会按key重新计算在数组中的index,如果index跟当前位置相等,则说明key处于正常的位置,如果不想等说明发生了Hash冲突,而当前正在执行清理过期数据的逻辑,所以前面有可能有过期数据被清理掉,需要把这个正常数据放入到正确的index。

ThreadLocalMap set数据流程,大体说一下。

根据key的hashcode进行寻址算法,找到index的位置,如果当前位置为null,则直接添加数据,如果不为null,则判断key是否相等,相等则替换,否则就是发生了hash冲突,需要线性探测法向后面遍历数据找到为null的位置插入,如果在查找过程中遇到相等的key,则进行更新。并且在set数据时,碰到过期数据量需要做替换

set数据时碰到过期数据了,需要做替换逻辑,这个替换逻辑是怎么做的?

从当前过期位置的下一个位置开始查找,直到碰到null 或者 相等的key才停止:如果碰到key一致,则set这个数据直接更新到当前这个entry位置即可,当前的entry与过期的位置进行互换 ; 如果碰到null,则直接在当前过期位置set数据。

如何共享ThreadLocal数据

在主线程使用InheritableThreadLocal实例,在子线程里面就可以得到这个实例。InheritableThreadLocal在创建的时候,如果父线程的InheritableThreadLocal存在就会赋值给当前线程的InheritableThreadLocal。

如何解决内存泄露?

ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

解决方案:在finally语句块里面执行 remove 方法清空值就ok。

那为什么ThreadLocalMap的key要设计成弱引用

因为不设置成弱引用那么key就不会被回收,会造成内存泄露。


目录