ThreadLocal 面试
ThreadLocal
ThreadLocal是一个关于创建线程局部变量的类。
通常情况下,我们创建的成员变量都是线程不安全的。因为他可能被多个线程同时修改,此变量对于多个线程之间彼此并不独立,是共享变量。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程无法访问和修改。也就是说:将线程公有化变成线程私有化。
- 比如存储 交易id等信息。每个线程私有。
- 比如aop里记录日志需要before记录请求id,end拿出请求id,这也可以。
- 比如jdbc连接池(很典型的一个
ThreadLocal
用法)
Thread
对象中都持有一个ThreadLocalMap
的成员变量。ThreadLocalMap
内部又维护了N个Entry
节点,也就是Entry
数组,每个Entry
代表一个完整的对象,key是ThreadLocal
本身,value是ThreadLocal
的泛型值和Synchronized的区别
存储在jvm的哪个区域
InheritableThreadLocal
类可以实现多个线程访问ThreadLocal
的值.会导致内存泄漏么
- 1、
ThreadLocalMap.Entry
的key会内存泄漏吗?
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
继承关系,发现是继承了弱引用,而且key直接是交给了父类处理super(key)
,父类是个弱引用,所以key完全不存在内存泄漏问题,因为他不是强引用,它可以被GC回收的。
- 2、
ThreadLocalMap.Entry
的value会内存泄漏吗?
value,发现value是个强引用,但是想了下也没问题的呀,因为线程终止了,我管你强引用还是弱引用,都会被GC掉的,因为引用链断了(jvm用的可达性分析法,线程终止了,根节点就断了,下面的都会被回收)。
这么分析一点毛病都没有,但是忘了一个主要的角色,那就是线程池,线程池的存在核心线程是不会销毁的,只要创建出来他会反复利用,生命周期不会结束掉,但是key是弱引用会被GC回收掉,value强引用不会回收,所以形成了如下场面:
Thread->ThreadLocalMap->Entry(key为null)->value
由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,早晨内存泄漏,时间久了必定OOM。
解决方案ThreadLocal
已经为我们想好了,提供了remove()
方法,这个方法是将value移出去的。所以用完后记得remove()
。
ThreadLocal里的对象一定是线程安全的吗
ThreadLocal.set()
进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()
获取的还是这个共享对象本身,还是有并发访问线程不安全问题。为什么用Entry数组而不是Entry对象
业务代码能new好多个ThreadLocal
对象,各司其职。ThreadLocalMap
是同一个,而不是多个,不管你new几次ThreadLocal
,ThreadLocalMap
在一个线程里就一个,因为ThreadLocalMap
的引用是在Thread
里的,所以它里面的Entry
数组存放的是一个线程里你new出来的多个ThreadLocal
对象。
作者:李二狗
链接:https://www.zhihu.com/question/341005993/answer/1996544027
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
问:
说一下 ThreadLocal 原理, java8
答:
java8, 每个线程对应的 Thread 对象内部有一个 ThreadLocals 字段, 这个字段指向堆中的一个ThreadLocalMap.
这个 ThreadLocalMap 内部存储的是,当前线程与其他 ThreadLocal 对象关联的数据。
Thread 这个线程对象,里面有一个 map 对象,这个map 存的是ThreadLocal 对象关联的数据。
问:
它是怎么做到线程,互不干扰的。
答:
线程有一个自己的 THreadLocalMap 存储数据。
线程访问某个 ThreadLocal 对象 get 方法时,会检测 当前线程 map 内部是否有 key 为这个 ThreadLocal 对象的 Entry 数据。
如果没有,这个 ThreadLocal 的 initial Value 方法 会创建一个 Entry 然后存放到这个 ThreadLocalMap
问:
jdk 1.8 之前的 版本怎么设计的
答:
老版本会在,TreadLocal 里面维护一个大 map, 所有线程的变量都会维护在一个 map 里面。
问:
jdk 8 和 之前的版本有什么优势。
答:
老版本维护一个大的 map, 线程多的话, 这个map 会很大。不利于维护。
新的版本。每个线程都会维护自己的数据,当线程被销毁的时候,线程对应额 ThreadLocalMap 在下次 GC 的时候被回收了。
还有这个 ThreadLocalMap 中的 Entry 存的 key 是弱引用,如果 ThreadLocal 对象被回收的话,是不影响的即弱引用不参与 root 算法。
问:
使用的 Hash 是从 Object 继承下来的 hashCode 方法吗?
答:
不是,这个是自己重写的,用一个黄金分割数来分割,均匀的分布在 Entry 数组里面。
如果 从 Object 继承的 HashCode 计算出来的 hash 值是不均匀的。 如果用黄金分割数,分配 hash 值,映射到散列表内部就很均匀。
比如长度为 16 分配四个,就 table[0] table[4] table[8] table[12], 反映到散列表,就很均匀。
问:
为什么 TheadLocalMap 使用 自定义 map, 而不是 jdk 的 HashMap
答:
重写的话,可以把这个 key 为限定为特有类型,就是 ThreadLocal 这个类型,key 是弱以用。
TheadLocal 这个写数据和查数据过程中,有清理过期数据的策略。能够将过期数据清理掉,解决了内存泄漏问题。
TheadLocal 的 value 的引用, 如果是对应的数据是过期的话,就会被干掉,
问:
每个线程的 ThreadLocalMap 对象是什么时间创建的
答:
每个线程的 ThreadLocalMap 是延迟初始化的
第一次调用get 或者 set 时候,检测当前线程是否绑定 ThreadLocalMap,
如果有就继续 get 或者 set, 如果没有, 就先创建。
问:
那么这个线程会不会被多次创建?
答:
在线程的生命周期内,ThreadLocalMap 只会初始化一次
问:
这个 map 初始化长度是多少
答:
16
问:
为什么这个长度,是 2 的次方数
答:
和 hashMap 一样,方便 hash 寻址。 因为 2 的次方数减一之后转变为 二级制由 1 组成,
如果数值与二进制位与运算,得到的数,大于等于0 且小于等于这个二进制数值,比取模算法,即%,效率高很多。
即 因为使用的是 位运算,所以效率高。
问:
扩容阈值时多少, 它达到扩容阈值一定会扩容吗
答:
entry 数组的 2/3。
但是不一定会扩容,它会 rehash 一次, 调用 rehash 方法。
全量扫描整个散列表的逻辑,把过期数据清理掉,
如果全量扫描完后,当前散列表的数据仍然达到这个扩容阈值 3/4, 才真正进行扩容.
问:
这个扩容算法是什么
答:
首先,创建一个新的数组,长度是当前散列表数组的两倍,迭代老的数组,将其中的数组,按照 hash 算法放入,新的数组里边。
迭代完后,这个数组就迁移完了。然后更新 ThreadLocalMap 对象的散列表引用。它会指向这个新的数组引用,扩容基本完成。
(细节) 扩容之后,还会重新计算下次扩容的阈值。
问:
ThreadLocal Map Get 的逻辑
答:
根据这个 ThreadLocal 对象的 hash 值 按位与 , 当前数组长度减一 得到一个 index
这个散列表数组中,下标就是这个 index 的元素,可能就是要查找的数据。如果查找的地方,发生过 hash 冲突,因为 ThreadLocal
内部类,Entry 没有 next 这个字段,ThreadLocal 采用的是 hash冲突后,线性的找到一个合适的位置去写数据。
如果 get 没有命中的话,就要继续向后查找,直到找到这个数据或者碰到 null 就结束。同时,还会遍历当前数据是否过期。
问:
假如第一次 get 没有 get 到, 如果查找过程中碰到过期数据,怎么处理
答:
首先,过期数据是什么。
ThreadLocal 内部存的 是 Entry, Entry 有两个字段,分别是 key 和 value, key 是一个弱引用。指向内存,已经限定类型的 ThreadLocal 对象。
value 就是当前线程的关联对象, 当 key 对应的 ThreadLocal 对象被 GC 回收后,
以为 key 是 弱引用,所以 key 的 get 方法 可能会 get 一个 指向 null 的一个引用,就这个 Entry 是过期的。
再说一下,get 查询过程,碰到 过期数据怎么处理
先会触发 “探测式” 过期数据回收逻辑, 就是从当前桶位开始向后迭代, 碰到 key == null 的 Entry 设置为 nll,一直迭代到 slot == null 为止。
向下迭代过程中如果遇到正常数据,会根据 key 重新重新计算一个 index, 如果等于,index 是否等于 当前位置,如果等于,就相当于啥也不做,
因为写入时,可以认为没有发生过 hash 冲突。如果重新计算的 index 不等于当前位置,说明发生了 hash 冲突, 当前数据的slot之前可能 有过期数据被干掉。
正常数据需要重新寻找一个更合适的位置去存放数据,这个位置理论上更接近或等于于正确的 index。
问:
Set 的流程
答:
根据 key 找到 对应下标的 slot ,如果 slot 为 null , 说明当前 set 方法是,新添加数据的逻辑。
如果这个 slot 不是 null, 那情况就比较复杂。两种情况
第一种,添加新的逻辑,但是发生 hash 冲突, 就线性找到可以使用的 slot 然后插入。
第二种,是更新的逻辑,如果过程查到 key 和 set 的 key 一致的话,发生Entry替换 value,
如果查找到过期数据,就做一个替换逻辑。
问:
set 过程中,替换过期数据的逻辑是怎么样。这个挺难的,还记得吗
答:
它会以当前位置的下一个桶位开始向后去查找,直到碰到 null 或者 key 一致才会停止。
第一种情况,就是碰到当key 一致的时候,那么set 的这个数据,直接就更新到当前这个桶位的这个Entry,就可以了,就更新逻辑。
然后让,当前的 Entry 与过期的 slot 进行一次互换。
第二种情况,遍历到null , 也没有找到key 一致的数据,
那么直接在当前过期桶位直接重写一个Entry 就 ok了,相当于抹除过期数据,将新的数据放到这里。
还涉及到启发式过期数据的清理的逻辑。
https://www.bilibili.com/video/BV19C4y1W72V?t=519