ThreadLocal面试突击!超级详细

声明

本文用于面试总结,没有基础的同学,可以看这个视频:
java基础教程由浅入深全面解析threadlocal

什么是ThreadLocal?

在这里插入图片描述
大意:ThreadLocal类提供线程局部变量,这些变量与其他普通共享变量在于,每一个线程都有自己独立的一份进行过初始化复制的变量,ThreadLocal的实列通常在类中是private static的,这样做是希望将实列的状态与Thread关联。

结构演变

在JDK8前,ThreadLocal中会维护一个ThreadLocalMap,这个ThreadLocalMap中包含一个Entry数组,Entry的key值是当前Thread,value是要存储的变量值。
在这里插入图片描述
在JDK8后进行了优化,每个线程中维护一个ThreadLocalMap,key值变为ThreadLocal对象,value不变,这样的好处有两点:

  1. 在JDK8前,每多一个线程,就意味着多一个key,key越多,意味着hash冲突也就越多,而JDK8将key修改为ThreadLocal,ThreadLocal的数量往往是小于Thread的数量。
  2. 在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循环中定义了两种情况:

  1. 当k==key,key值相同,执行更新操作。
  2. 当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无法访问,从而导致内存泄漏。

扫描二维码关注公众号,回复: 12444998 查看本文章

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使用完后就释放,减少占用内存空间。

猜你喜欢

转载自blog.csdn.net/TheCarol/article/details/111938620