声明
本文用于面试总结,没有基础的同学,可以看这个视频:
java基础教程由浅入深全面解析threadlocal
什么是ThreadLocal?
大意:ThreadLocal类提供线程局部变量,这些变量与其他普通共享变量在于,每一个线程都有自己独立的一份进行过初始化复制的变量,ThreadLocal的实列通常在类中是private static的,这样做是希望将实列的状态与Thread关联。
结构演变
在JDK8前,ThreadLocal中会维护一个ThreadLocalMap,这个ThreadLocalMap中包含一个Entry数组,Entry的key值是当前Thread,value是要存储的变量值。
在JDK8后进行了优化,每个线程中维护一个ThreadLocalMap,key值变为ThreadLocal对象,value不变,这样的好处有两点:
- 在JDK8前,每多一个线程,就意味着多一个key,key越多,意味着hash冲突也就越多,而JDK8将key修改为ThreadLocal,ThreadLocal的数量往往是小于Thread的数量。
- 在JDK8前,ThreadLocal不会随着Thread的结束而释放内存,而JDK8在Thread使用完后ThreadLocal就会释放,减少缓存的占用。
弱引用
在JVM中,当指向一个对象的引用只有弱引用时,这个对象在任何时刻都有可能被GC回收。
这样做的好处:当ThreadLocal使用完后,ThreadLocalRef就会断开,此时的ThreadLocal只有key的弱引用,这样就可以被GC回收。同时当ThreadLocal被回收时,key的值就变为null,在Thread的getEntry和set方法中都会将key为null的Entry的value也变为null,这样进一步减少了内存的占用。
内存泄漏
出现内存的根本原因就是:由于ThreadLocalMap是Thread的一个属性,其生命周期与Thread一样,这样CurrentThreadRef这个强引用就一直不会断,同时由于强应用使得GC不能执行回收,这样就导致CurrentThreadRef-CurrentThread-ThreadLocalMap-Entry[] 形成了一条强引用链,如果在ThreadLocal使用完后不将对应Entry删除,那么这个Entry就会一直存在且无法被访问,从而导致了内存泄漏。
Hash冲突
ThreadLocal并不是和HashMap一样采用拉链发解决冲突,而是采用线性探测法。
从当前发生冲突的位置开始按照顺序遍历,for循环中定义了两种情况:
- 当k==key,key值相同,执行更新操作。
- 当k==null,就是ThreadLocal已经被GC了,执行replaceStaleEntry将Entry替换掉。
如果for循环执行完后,任然没有找到位置,那么就创建一个新的Entry插入进去,同时需要考虑要不要扩容类。
cleanSomeSlots中会按照一定方法寻找stale的Entry,找到就将其remove掉并返回true,没找到就返回false。
threshold与HashMap中的类似,两个条件同时满足时就进行rehash。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
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 的 key 是弱引用?
如果是强引用的话,threadlocal由于被key强引用了就无法被gc,那么key就一直有对应值,value也无法被回收。
如果为弱引用threadlocal就可以被gc,那么key就为null,而在set/get entry方法里,当key为null时,会将value也置为null。
ThreadLocal 的应用场景有哪些?
其中spring中的事务管理器就是使用的ThreadLocal,使得同一线程内获取的connection是同一对象。
ThreadLocal 的内存泄露是怎么回事?
根本原因是threadlocalmap和thread的生命周期一样长,在使用完threadlocal后不调用remove删除对应的Entry的话,就会导致。虽然map中弱引用的threadlocal会被gc,但由于thread强引用threadlocalmap,threadmap中entry占用的内存空间不会被gc掉,但是由于key的threadlocal已经被gc了,这个entry无法访问,从而导致内存泄漏。
ThreadLocal 如何解决 Hash 冲突?
采用线性探测法,当发现这个位置已经被占用后,就会按顺序查找下去,直到空位就插入,如果遍历完后还是没有找到就new一个entry进去,此时new一个进去后还需要判断是否要扩容。
ThreadLocal 工作原理是什么?
在jdk8前,threadlocal中包含一个threadlocalmap,这个map的key值为thread,value为要存储的变量,而在jdk8,每个线程中都包含一个threadlocal,threadlocal中维护一个map,此时map的key为threadlocal本身,value任然为存储的变量,这样可以减少key的个数,jdk8之前,thread越多,key就越多。同时threadlocal会随着thread使用完后就释放,减少占用内存空间。