ThreadLocal源码夺命12问,你能坚持到第几问?

ThreadLocal源码面试

1、面试官:请你说一说你对ThreadLocal的理解?

  • ThreadLocal是一个全局对象,ThreadLocal是线程范围内变量共享的解决方案;threadLocal可以看作是一个map集合,key就是当前线程对象,value就是要存放的变量。

ThreadLocal对象可以给每个线程分配一份属于自己的局部变量副本,多个线程之间可以互不干扰。一般我们会重写initivalue()方来给当前ThreadLocal对象赋初始值。

2、面试官:简单描述一下JDK1.8中,ThreadLocal原理?

  • JDK8中,每个线程对象Thread类内部都有一个成员属性threadLocals(即ThreadLocalMap,,它是一个Entry[]数组,而不是Map集合),各个线程在调用同一个ThreadLocal对象的set(value)方法设置值的时候,就是往各自的ThreadLocalMap对象数组中新增值。
  • ThreadLocalMap(Entry[]数组)中存放的是一个个的Entry节点,它有两个属性字段,弱引用key(ThreadLocal对象),和强引用value(当前线程变量副本的值)。

3、面试官:ThreadLocal是怎么样做到线程互不干扰的呢(线程隔离)?

  • 首先,每个线程Thread都有一份自己的ThreadLocalMap用于存储数据。
  • 当线程访问某个ThreadLocalMap对象的get()方法的时候,方法内部会检测该线程的ThreadLocalMap数组内是否存在key为当前对象的Entry节点,如果数组内没有对应的节点,那么当前的ThreadLocal对象就会调用内部的initlalValue()方法创建一个Entry节点存放到ThreadLocalMap中去。

4、面试官:ThreadLocal使用的hash是怎么样计算得来的?

  • 首先,ThreadLocal使用的hash并不是重写自Object的hashCode()方法,而是通过自身nextHashCode();计算得来。代码如下:
/**
     * threadLocalHashCode ---> 用于threadLocals的桶位寻址:
     * 1.线程获取threadLocal.get()的时候:
     *  如果这个是第一次在某个threadLocal对象上get,那么就会给当前对象分配一个value
     *  这个value和当前的threadLocal对象被包装成为一个entry对象,其中entry的key是threadLocal对象
     *  value是threadLocal给当前线程生成的value
     * 2.这个entry存放到当前线程的threadLocals这个map的哪个桶位呢?
     *  桶位寻址与当前threadLocal对象的threadLocalHashCode有关系
     *  使用当前threadLocal对象threadLocalHashCode & (table.length-1)
     *  计算结果得到的位置就是当前entry在Entry[] table中需要存放的位置
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * nextHashCode:表示hash值
     * 创建ThreadLocal对象的时候会使用到该属性
     * 每创建一个threadLocal对象的时候,就会使用nextHashCode分配一个hash值给这个对象
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * HASH_INCREMENT、:表示hash值的增量
     * 每创建一个ThreadLocal对象,ThreadLocal.HASH_INCREMENT(0x61c88647)
     * 这个值很特殊,它是斐波那契数也叫黄健分割数
     * hash增量为这个数字,带来的好处是hash分布非常均匀
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * 返回一个nextHashCode的hash值:
     * 创建新的ThreadLocal对象的时候,使用这个方法,会给当前对象分配一个hash值
     * @return
     */
    private static int nextHashCode() {
        //每创建一个ThreadLocal变量,nextHashCode计算得到的hash值就增长HASH_INCREMENT(0x61c88647)
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    /**
     * 返回此线程(即当前线程的)局部变量的初始值
     * 此方法在每个线程第一次调用get()方法的时候调用(前提是没有调用set方法),如果调用了
     * set方法,那么该方法将不再执,每个线程最多最多调用一次,但是在发生错误的时候
     * 可以再次调用 初始化一个起始value 一般情况下都是需要重写这个方法的
     * @return
     */
    protected T initialValue() {
        return null;
    }

5、面试官:为什么ThreadLocalMap选择去重新设计MAP,而不直接使用JDK中的HashMap呢?

  • 因为ThreadLocal自己重新设计的MAP,它可以把自己的key限定为特有类型(ThreadLocal),这个特定类型的key使用的是弱引用WeakReference,而HashMap中的key采用的是强引用方式。

6、面试官:ThreadLocalMap中的Entry的 key为什么要设置成弱引用?

  • ThreadLocalMap存储的格式是Entry<ThreadLocal,T>,如果使用强引用,当key原来对象失效的时候,JVM不会回收map里面的ThreadLocal对象。
  • 弱引用WeakReference定义:如果一个对象只具有弱引用,那么垃圾回收器在扫描到该对象的时候,无论内存是否充足,都会回收该对象的内存。
  • ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用它,那么系统在GC的时候,这个ThreadLocal势必会被回收,这样依赖,ThreadLocalMap中就会出现key为null的Entry,就没有办法来访问这些key为null的Entry的 value。
  • 站在ThreadLocalMap的角度就可以区分出哪些Entry是过期的,哪些Entry是非过期的
    • 例如:在set()方法向下寻找可用slot桶位的过程中,如果碰到key==null的情况,说明当前Entry是过期的数据,这个时候可以强行占用该桶位,通过replaceStaleEntry方法执行替换过期的数据的逻辑。
    • 例如:cleanSomeSlots(int i,int n)方法通过遍历桶位,也会将key==null过期数据清理掉。

7、面试官:ThreadLocalMap对象是何时第一次被创建呢?

  • 每个线程Thread对象的ThreadLocalMap都是延迟初始化的,当我们调用ThreadLocal对象的set()或者get()方法的时候,它会检测当前线程是否已经绑定了ThreadLocalMap,如果绑定了,则继续执行set()或者get()方法的逻辑。
  • 而如果没有,则会先创建ThreadLocalMap并将其绑定给Thread对象。

面试官:那么线程的ThreadLocalMap会被多次创建吗?

不会,在线程的声明周期内,ThreadLocalMap对象只会被初始化一次。

8、面试官:ThreadlocalMap的初始化长度是多少呢?

初始化的时候,ThreadLocalMap对象的容量为16。

9、面试官:上面你说初始化长度是16,那为什么初始容量要是2的N次幂数呢?

  • 这个设计它和HashMap是一样的,目的都是为了方便hash寻址的时候,得到的桶位均匀分布,减少hash冲突。
  • 寻址算法为:index=threadLocalHashCode & (table.length-1)。这个算法实际就是取模运算:hash%tab.length,而计算机中求余运算不如位移运算。
  • 所以在源码中做了优化,使用hash & (table.length-1)来寻找桶位,而实际上,hash%length等于hash & (length -1)的前提是length必须为2的n次幂。
  • 例如:数组长度table.length=8的时候,3 & (8-1)=3, 2 & (8-1)=2,桶的位置是(数组索引)3和2,不同位置上,不发生hash碰撞。

10、面试官:ThreadLocalMap的扩容阙值是多少?它的扩容机制是怎样的?

  • 首先,ThreadLocalMap的扩容阙值为初始容量的2/3,当数组中,存储的Entry节点的个数大于等于2/3的时候,它并不会直接开始扩容
  • 而是先调用rehash()方法,在该方法中,全面扫描整个数组,并将数组中过期的数据(key==null)给清理掉,重新整理数组。
  • 如果重新整理数组,并将过期的数据清理后,再次判断数组内的Entry节点的个数是否达到扩容阙值的3/4,如果达到再调用真正扩容的方法resize()方法;

面试官:那么你对resize()方法内部的扩容算法了解吗?

  • resize()方法再真正执行扩容的时候,内部逻辑是先创建一个新的数组,新数组长度是原来数组长度的2倍。
  • 然后遍历旧数组,将旧数组中的数据重新按照hash算法前移到新数组里面。
  • 接着重新计算出下次扩容的阙值thresold
  • 最后更新Thread对象的threadLocas字段引用,使其指向新数组。

11、面试官:请你说一下threadLocal的get方法的执行过程?

  • 1、首先get()方法会先获取当前的线程对象t:Thread t=Thread.currentThread();
  • 2、接下来会根据t来获取其独有的ThreadLocalMap数组:ThreadLocalMap map=getMap(t);
  • 3、如果第二步中获取的map为空,则调用setinitialValue()方法,该方法内部调用initialValue();方法获取默认的value,并根据当前线程t和value调用createMap(t,value);方法创建ThreadLocalMap。
  • 4、如果第二步中获取的map不为空,则直接调用ThreadLocalMap.Entry e=map.getEntry(this);方法通过this(当前ThreadLocal对象)从ThreadLocalMap中获取对应封装数据的Entry节点。
  • 5、最终通过T result=(T)e.value;得到要获取的线程遍历副本的值。

注意:

第四步中,通过当前ThreadLocal对象从ThreadLocalMap中获取对应封装数据的Entry节点的时候,内部逻辑是需要涉及到桶位寻址index=threadLocalHashCode & (table.length-1),如果获取的index桶位中没有目标数据或者并且当前位置的key与条件的key不同,这时候会执行nextIndex(int i,int len)方法,线性的向后或者向前去寻找目标数据所在的桶位,直到遍历整个数组仍未找到,则返回null。

此外,在线性的向前或者向后遍历数组寻找目标元素所在的桶位的时候,如果发现数据过期了(key==null),则需要调用expungeStaleEntry(i);方法进行一次探测式过期数据回收。

12、面试官:请你说一下ThreadLocal的set方法的执行流程?

  • 1、首先,set()方法向ThreadLocalMap中添加数据的时候,也是需要根据key(ThreadLocal对象)去寻址找到要插入的桶位下标i=key.threadLocalHashCode & (len-1);
  • 2、根据桶位下标,获取对应桶中的Entry对象,如果获取到的Entry e=tab[i];如果获取到的e位null,则说明是空桶,直接将key和value包装成Entry放入桶中即可:tab[i]=new Entry(key,value);
  • 3、如果第二步中获取到的 e不为空,说明不是空桶,则需要从以下三种情况来考虑:
    • 如果当前桶中Entry的key不是当前ThreadLocal对象,且不为null,则调用nextIndex(it i,int len)方线性查找下一个空桶位,并将新数据放入。
    • 如果当前桶中Entry的key是当前ThreadLocal对象,则通过更新操作,将就Entry的value值覆盖。
    • 如果当前桶中的Entry的key为null,则说明当前Entry已经过期,需要执行替换过期的数据的逻辑:replaceStaleEntry(key,value,i)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值