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就不会被回收,会造成内存泄露。