ThreadLocal原理详解

60 阅读3分钟

使用场景

  1. 系统Looper使用ThreadLocal来确保每个线程只有一个looper对象。
  2. 平时业务中,可以使用ThreadLocal来确保每个线程维护自己的一个实例和数据。

ThreadLocal 整体功能实现结构

ThreadLocal-1.png

    ThreadLocal和线程相关,每个线程Thread中会维护自己的一个ThreadLocalMap对象实例,顾名思义,该实例就用于储存ThreadLocal对象所对应的value。

    ThreadLocalMap中维护了一个Entry数组,Entry对象保存了ThreadLocal对象和其对应的值。

    这里Entry继承自 WeakReference,其中ThreadLocal对象使用了弱引用,这里为的是防止内存泄露,可以及时回收ThreadLocal对象。

关于set和get方法

    我们使用ThreadLocal时候,都是直接new一个ThreadLocal,通过其暴露的set和get方法来设置或者获取目标值。我们并不会直接操作其所在线程或线程内的ThreadLocalMap,对于这些我们无感知。

  • 使用

    ThreadLocal<String> stringThreadLocal = new ThreadLocal<>(); stringThreadLocal.set("123"); String s123 = stringThreadLocal.get();

set方法

    我们通过第一个图可以知道其内部原理结构是通过一个数组来维护了ThreadLocal和其value的映射关系。那么set方法肯定也是寻找对应的ThreadLocalMap对象,然后将ThreadLocal对象和value放到对应的Entry数组中。

ThreadLocal-2 (1).png

    如何找到ThreadLocal在数组中的位置呢?

  1. 当我们生成一个ThreadLocal对象时候,其内部会对应生成一个hashCode,如下图,从第一个ThreadLocal对象开始,后续生成的hashCode都会依次增加一个 HASH_INCREMENT ,从而保证了当前线程内每个ThreadLocal对象中的Hashcode都不一样,且依次递增。

image.png

  1. 对该hashCode和数组长度-1做与,得到其在数组中的位置。
  2. 但是可能会出现两个ThreadLocal的hashCode和数组长度做与后,得到一样的数组下标的情况。这个时候就需要处理冲突了。
  • 处理下标冲突
  1. 从hashCode和数组长度-1做与后得到的下标开始
  2. 依次向后查找,优先比对key是否相等,相等则直接替换value,检查Entry中的key是否为NULL,如果为NULL则说明当前位置下的ThreadLocal被回收了,接下来调用方法replaceStaleEntry来进行回收+替换值处理。最后如果是Entry为NULL,那么说明当前数组中没有设置过当前ThreadLocal的值,直接新生成一个,放到当前位置即可。

image.png

  • 方法 replaceStaleEntry

    该方法主要用于查找key被回收的entry对象的位置下标,然后从该下标开始,对无用的entry对象进行回收,同时重新分步这过程中涉及到的entry对象在数组中的位置。

    图中slotToExpunge的含义:第一个key被回收的entry对象的下标。

ThreadLocal-3 (1).png

  • get方法

    get方法也是通过ThreadLocal的哈数Code来查找对应下标,比对key查找value。需要注意点的是,如果查找过程中发现key为null,也会触发回收和重新分配Entry位置的机制。

ThreadLocal-4.png