问:
说一下 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