ThreadLocal 面试

ThreadLocal

  ThreadLocal是一个关于创建线程局部变量的类。

  通常情况下,我们创建的成员变量都是线程不安全的。因为他可能被多个线程同时修改,此变量对于多个线程之间彼此并不独立,是共享变量。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程无法访问和修改。也就是说:将线程公有化变成线程私有化。

使用场景
  • 比如存储 交易id等信息。每个线程私有。
  • 比如aop里记录日志需要before记录请求id,end拿出请求id,这也可以。
  • 比如jdbc连接池(很典型的一个ThreadLocal用法)
数据结构
  每个Thread对象中都持有一个ThreadLocalMap的成员变量。
  每个ThreadLocalMap内部又维护了N个Entry节点,也就是Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值

和Synchronized的区别

  Synchronized同步机制保证的是多线程同时操作共享变量并且能正确的输出结果。ThreadLocal不行啊,他把共享变量变成线程私有了,每个线程都有独立的一个变量。
 

存储在jvm的哪个区域

  还是在堆的。ThreadLocal对象也是对象,对象就在堆。只是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几次ThreadLocalThreadLocalMap在一个线程里就一个,因为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

 

 

 

 

 

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

posted @ 2020-10-22 13:54  李荣先辈Java  阅读(288)  评论(0编辑  收藏  举报