1、Thread、 ThreadLocal 及 ThreadLocalMap

ThreadLocal提供了一种方式,可以让线程在操作共享变量时,复制该共享变量的一个副本到线程自己的栈空间,以后就操作这个副本空间来代替共享空间。

Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系
image.png

每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对,key 就是 ThreadLocal 的引用,value是我们希望 ThreadLocal 存储的内容,例如 user 对象等。

2、ThreadLocal

2.1、ThreadLocal属性

threadLocalHashCode属性决定了ThreadLocal -> value键值对保存在线程属性ThreadLocalMap 中的桶位,桶位 = threadLocalHashCode & (table.length - 1)

  1. public class ThreadLocal<T> {
  2. /**
  3. * 线程在使用ThreadLocal.get()方法时,如果是第一次在threadLocal对象上get,会给当前线程分配一个value
  4. * 该value和ThreadLocal对象被包装成一个entry,其中key为ThreadLocal对象,value为给当前线程分配的value
  5. * 这个entry存放到当前线程threadLocals这个map中,而entry在map中的桶位跟ThreadLocal对象的threadLocalHashCode属性值有关
  6. * 桶位 = threadLocalHashCode & (table.length - 1)
  7. */
  8. private final int threadLocalHashCode = nextHashCode();
  9. /**
  10. * 创建ThreadLocal对象时,给每个ThreadLocal对象分配Hash值的,每创建一个ThreadLocal对象
  11. * 使用nextHashCode分配一个Hash值给这个对象
  12. */
  13. private static AtomicInteger nextHashCode =
  14. new AtomicInteger();
  15. /**
  16. * 每创建一个ThreadLocal对象,ThreadLocal.nextHashCode静态属性值会增长的大小
  17. * 0x61c88647很特殊,是斐波拉切数也叫黄金分割数,使用该数可以使hash值分布非常均匀
  18. */
  19. private static final int HASH_INCREMENT = 0x61c88647;
  20. /**
  21. * 给新创建的ThreadLocal对象分配nextHashCode时需要调用的方法
  22. */
  23. private static int nextHashCode() {
  24. return nextHashCode.getAndAdd(HASH_INCREMENT);
  25. }
  26. /**
  27. * 赋值方法 一般会重写
  28. * @return
  29. */
  30. protected T initialValue() {
  31. return null;
  32. }
  33. public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
  34. return new SuppliedThreadLocal<>(supplier);
  35. }
  36. public ThreadLocal() {
  37. }
  38. }

2.2、ThreadLocal对外方法

2.2.1、ThreadLocal#set

保存键值对,如果当前线程ThreadLocalMap属性初始化过,直接调用ThreadLocalMap#set方法覆盖或者新放入对应的键值;如果当前线程没有初始化过ThreadLocalMap属性,则直接调用ThreadLocal#createMap新建一个ThreadLocalMap,并将当前ThreadLocal对象和对应的值存入。

  1. /**
  2. * 修改当前线程保存的当前ThreadLocal对象的对应的value值,
  3. *
  4. * @param value
  5. */
  6. public void set(T value) {
  7. Thread t = Thread.currentThread();
  8. //获取当前线程对象的threadLocals属性
  9. ThreadLocalMap map = getMap(t);
  10. if (map != null) {
  11. //如果当前线程ThreadLocalMap属性初始化过,直接调用set方法覆盖或者新放入对应的键值
  12. map.set(this, value);
  13. } else {
  14. //如果当前线程没有初始化过ThreadLocalMap属性,则直接新建一个ThreadLocalMap,并将当前
  15. //ThreadLocal对象和对应的值存入
  16. createMap(t, value);
  17. }
  18. }

2.2.2、ThreadLocal#get

获取当前线程保存的键值对,调用ThreadLocalMap#getEntry方法获取存储的键值对,如果获取的key在ThreadLocalMap中没有或者ThreadLocalMap没有初始化,则先初始化当前线程的ThreadLocalMap属性,然后设置获取key的默认值

  1. /**
  2. *返回当前线程与当前ThreadLocal对象相关联的线程局部变量 ,该变量只有当前线程能访问到
  3. * 如果当前线程没有分配变量,则使用initialValue方法给当前线程分配一个变量
  4. * @return
  5. */
  6. public T get() {
  7. Thread t = Thread.currentThread();
  8. //返回当前线程的ThreadLocal.ThreadLocalMap threadLocals 属性
  9. ThreadLocalMap map = getMap(t);
  10. /**
  11. * map != null 成立 说明当前线程已经初始化过ThreadLocalMap 属性了
  12. */
  13. if (map != null) {
  14. //根据当前的ThreadLocal对象去线程的ThreadLocalMap中获取value
  15. ThreadLocalMap.Entry e = map.getEntry(this);
  16. //e != null成立说明当前线程ThreadLocalMap中保存有对应ThreadLocal对象的value
  17. if (e != null) {
  18. @SuppressWarnings("unchecked")
  19. T result = (T)e.value;
  20. //返回对应的value值
  21. return result;
  22. }
  23. }
  24. /**
  25. * 走到这里有哪些情况?
  26. * 1、当前线程ThreadLocalMap 属性没有被初始化 需要走初始化逻辑
  27. * 2、根据当前的ThreadLocal对象去线程的ThreadLocalMap中获取value,无对应的value
  28. */
  29. return setInitialValue();
  30. }

ThreadLocal#setInitialValue 初始化线程ThreadLocalMap,设置查询未命中的初始值

  1. /**
  2. *初始化当前线程对应ThreadLocal对象的初始值,如果当前线程的ThreadLocalMap对象为空,
  3. * 初始化当前线程的ThreadLocalMap对象,
  4. * @return
  5. */
  6. private T setInitialValue() {
  7. //调用initialValue方法为线程对应的ThreadLocal对象生成默认的value
  8. T value = initialValue();
  9. Thread t = Thread.currentThread();
  10. ThreadLocalMap map = getMap(t);
  11. if (map != null) {
  12. //如果线程内部的ThreadLocalMap属性已经初始化,则保存ThreadLocal对象 和 当前线程对应的ThreadLocal对象生成默认的value
  13. map.set(this, value);
  14. } else {
  15. //如果ThreadLocalMap为空则对ThreadLocalMap初始化
  16. createMap(t, value);
  17. }
  18. if (this instanceof TerminatingThreadLocal) {
  19. TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
  20. }
  21. return value;
  22. }

ThreadLocal#initialValue设置默认初始值

  1. /**
  2. * 赋值方法 一般会重写
  3. * @return
  4. */
  5. protected T initialValue() {
  6. return null;
  7. }

2.2.3、ThreadLocal#remove

移除当前线程与当前ThreadLocal对象相关联的value,调用ThreadLocalMap#remove方法完成

  1. /**
  2. * 移除当前线程与当前ThreadLocal对象相关联的value
  3. */
  4. public void remove() {
  5. ThreadLocalMap m = getMap(Thread.currentThread());
  6. if (m != null) {
  7. //如果线程的ThreadLocalMap属性已经初始化了,直接调用ThreadLocalMap的remove方法
  8. m.remove(this);
  9. }
  10. }

3、ThreadLocalMap

3.1、ThreadLocalMap属性

ThreadLocalMap内部采用一个初始长度为16的数组保存数据,数组长度必须是2的次方,扩容阈值为散列表数组长度的2/3,扩容每次翻一倍。其向前向后访问都是循环的,第一个位置元素的上一个元素是最后一个元素,最后一个元素的上一个元素为第一个元素。

  1. /**
  2. * ThreadLocalMap 是用来存储 线程对应的ThreadLocal的值的自定义hash Map,
  3. *
  4. */
  5. static class ThreadLocalMap {
  6. /**
  7. * Entry 结构实际上是继承了一个ThreadLocal类型的弱引用并将其作为 key
  8. */
  9. static class Entry extends WeakReference<ThreadLocal<?>> {
  10. /** The value associated with this ThreadLocal. */
  11. Object value;
  12. Entry(ThreadLocal<?> k, Object v) {
  13. super(k);
  14. value = v;
  15. }
  16. }
  17. /**
  18. * 初始化ThreadLocalMap时散列表数组的长度 默认16
  19. */
  20. private static final int INITIAL_CAPACITY = 16;
  21. /**
  22. * 散列表数组 数组长度必须是2的次方,扩容每次翻一倍
  23. */
  24. private Entry[] table;
  25. /**
  26. * ThreadLocalMap的数据数量即散列表数组的占用情况
  27. */
  28. private int size = 0;
  29. /**
  30. * 扩容触发阈值 初始值为len * 2 / 3
  31. * 触发之后调用rehash()方法
  32. * rehash()方法会先做一次全量的过期数据检查,把散列表中所有的过期entry全部删除
  33. * 如果移除之后当前散列表的entry数量任然达到threshold * 3 /4,就进行扩容
  34. */
  35. private int threshold; // Default to 0
  36. /**
  37. * 设置扩容阈值为散列表数组长度的2/3
  38. */
  39. private void setThreshold(int len) {
  40. threshold = len * 2 / 3;
  41. }
  42. /**
  43. * 后一个元素位置(循环)
  44. * 参数1:当前下标
  45. * 参数2:当前散列表数组长度
  46. *
  47. * 如果i到达散列表数组最后一个元素位置下一步就变为0,访问数组的第一个元素,循环访问数组
  48. */
  49. private static int nextIndex(int i, int len) {
  50. return ((i + 1 < len) ? i + 1 : 0);
  51. }
  52. /**
  53. * 前一个元素位置(循环访问)
  54. * 参数1:当前下标
  55. * 参数2:当前散列表数组长度
  56. * 如果i为0,则上一个元素的位置为n-1,为数组最后一个元素
  57. */
  58. private static int prevIndex(int i, int len) {
  59. return ((i - 1 >= 0) ? i - 1 : len - 1);
  60. }
  61. /**
  62. * 构造一个包含第一个entry的ThreadLocalMap
  63. * @param firstKey 第一个键值对的key
  64. * @param firstValue 第一个键值对的value
  65. */
  66. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  67. //默认初始长度16
  68. table = new Entry[INITIAL_CAPACITY];
  69. //计算桶位
  70. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  71. //将第一个键值对放入散列表数组
  72. table[i] = new Entry(firstKey, firstValue);
  73. //更改size为1
  74. size = 1;
  75. //设置扩容阈值
  76. setThreshold(INITIAL_CAPACITY);
  77. }
  78. /**
  79. * 根据现有的ThreadLocalMap 构建一个新的ThreadLocalMap
  80. * @param parentMap 父ThreadLocalMap
  81. */
  82. private ThreadLocalMap(ThreadLocalMap parentMap) {
  83. Entry[] parentTable = parentMap.table;
  84. int len = parentTable.length;
  85. setThreshold(len);
  86. table = new Entry[len];
  87. for (Entry e : parentTable) {
  88. if (e != null) {
  89. @SuppressWarnings("unchecked")
  90. ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
  91. if (key != null) {
  92. Object value = key.childValue(e.value);
  93. Entry c = new Entry(key, value);
  94. int h = key.threadLocalHashCode & (len - 1);
  95. while (table[h] != null)
  96. h = nextIndex(h, len);
  97. table[h] = c;
  98. size++;
  99. }
  100. }
  101. }
  102. }
  103. }

3.2、ThreadLocalMap对外方法

3.2.1 ThreadLocalMap#set

特别注意ThreadLocalMap没有采用拉链法解决hash冲突,而是采用开放地址法,如果发送hash冲突,则向后查找最靠近的桶位。首先通过ThreadLocal对象的threadLocalHashCode属性计算桶位,桶位 = threadLocalHashCode & (table.length - 1),存放键值对时可能有多种情况:
1、通过key计算出的桶位上没有数据,直接赋值
2、通过key计算出的桶位上有数据,但是键值对的key和要设置的key相同,说明此时为重新赋值
3、发生hash冲突,继续向后遍历,找到entry的key等于要放入数据的key的位置,说明此时为重新赋值
4、发生hash冲突,继续向后遍历,找到entry的key为null,过期数据的位置,此时调用ThreadLocalMap#replaceStaleEntry替换过期数据
5、发生hash冲突,继续向后遍历,找到一个没有存放entry的位置,size +1,调用ThreadLocalMap#cleanSomeSlots进行启发式清理工作,如果没有数据被清理,并且数据量已经超过了扩容阈值,则调用ThreadLocalMap#rehash进行扩容

  1. /**
  2. * 调用set方法给当前线程添加ThreadLocal->value键值对
  3. * 调用ThreadLocal的set方法时实际调用的ThreadLocalMap的set方法
  4. * @param key
  5. * @param value
  6. */
  7. private void set(ThreadLocal<?> key, Object value) {
  8. Entry[] tab = table;
  9. int len = tab.length;
  10. //计算桶位 即entry存放在散列表数组的下标
  11. int i = key.threadLocalHashCode & (len-1);
  12. /**
  13. * for循环从当前桶位向后遍历 如果发生hash冲突,则继续向后遍历,直到找到一个可用的位置
  14. * 1、找到一个没有存放entry的位置
  15. * 2、找到entry的key等于要放入数据的key的位置(相同key元素重新赋值)
  16. * 3、找到entry的key为null,过期数据的位置
  17. *
  18. * 如果计算的桶位上没有entry则证明没有发生hash冲突,不执行while循环
  19. */
  20. for (Entry e = tab[i];
  21. e != null;
  22. e = tab[i = nextIndex(i, len)]) {
  23. //执行while循环证明发生了hash冲突或者是统一key重新赋值
  24. ThreadLocal<?> k = e.get();
  25. //当前桶位的key和要放入键值对的key相同 则证明为value重赋值
  26. if (k == key) {
  27. //value赋值 返回
  28. e.value = value;
  29. return;
  30. }
  31. //如果key为null,则证明key关联的ThreadLocal对象已经被回收了,该位置数据未过期数据
  32. if (k == null) {
  33. //替换过期数据
  34. replaceStaleEntry(key, value, i);
  35. return;
  36. }
  37. }
  38. /**
  39. * 执行到这里说明 当前位置entry为null,创建新的entry对象
  40. */
  41. tab[i] = new Entry(key, value);
  42. //size + 1
  43. int sz = ++size;
  44. /***
  45. * 进行启发式清理工作,如果没有数据被清理,并且数据量已经超过了扩容阈值,则进行扩容
  46. */
  47. if (!cleanSomeSlots(i, sz) && sz >= threshold){
  48. rehash();
  49. }
  50. }

3.2.2、ThreadLocalMap#getEntry

根据ThreadLocal对象从当前线程取出对应的value

  1. /**
  2. * 获取ThreadLocal对象关联的value
  3. *
  4. * ThreadLocal对象的get()方法实际调用的ThreadLocalMap的getEntry方法
  5. * @param key ThreadLocal对象
  6. * @return
  7. */
  8. private Entry getEntry(ThreadLocal<?> key) {
  9. //计算entry的位置 获取数据对应散列表数组的下标
  10. int i = key.threadLocalHashCode & (table.length - 1);
  11. Entry e = table[i];
  12. //如果对应位置的值不为空 并且对应entry的key为所查找的key 则证明该位置的entry就是要查找的entry 直接返回
  13. if (e != null && e.get() == key)
  14. return e;
  15. else{
  16. /**
  17. * 执行到这里有几种情况?
  18. * 1、当前key 查找到散列表数据对应位置数据为空
  19. * 2、发生了hash冲突,当前key查询到的entry的key和要查询的key不一致,需要向当前桶位后查询(ThreadLocalMap是通过开放地址法解决hash冲突的,并未使用拉链法)
  20. */
  21. return getEntryAfterMiss(key, i, e);
  22. }
  23. }

调用getEntry方法时,未获取到对应entry的逻辑

  1. /**
  2. * 通过key未查询到entry的处理逻辑
  3. * 1、通过key未查询到entry
  4. * 2、通过key查询到的entry的key和指定的key不同,发生了hash冲突
  5. * @param key ThreadLocal对象
  6. * @param i 通过查询的key计算出的桶位 即散列表数组的下标
  7. * @param e 查询出来的entry
  8. * 1、为空则说明 通过key未查询到entry
  9. * 2、不为空则说明 发生了hash冲突,通过key查询到的entry的key和指定的key不同,entry指向查出的冲突的entry
  10. * @return
  11. */
  12. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  13. Entry[] tab = table;
  14. int len = tab.length;
  15. //从发生hash冲突的entry循环向后访问
  16. while (e != null) {
  17. ThreadLocal<?> k = e.get();
  18. //k == key 说明找到了要查询的entry
  19. if (k == key)
  20. return e;
  21. /**
  22. * 元素的key为null(因为ThreadLocalMap的key为弱引用,如果key关联的ThreadLocal对象被GC回收了,那么key就会为null)
  23. * 说明当前位置的entry的key关联的ThreadLocal对象被GC回收了
  24. */
  25. if (k == null){
  26. //当散列表出现过期数据了 需要做一次探测式的过期数据检查和回收
  27. expungeStaleEntry(i);
  28. }
  29. //没有找到对应的entry 继续向后循环查找
  30. else
  31. i = nextIndex(i, len);
  32. //e为下一个元素
  33. e = tab[i];
  34. }
  35. /**
  36. * 此处有两种情况
  37. * 1、传入的e就为空,没有执行while循环,通过key未查询到entry,直接返回null
  38. * 2、传入的e不为空,通过key查询到的entry的key和指定的key不同,发生了hash冲突,执行了while循环,但是
  39. * 循环向后访问对象时,直到查询到entry为null,也没没找对应的entry(ThreadLocalMap采用开放地址法解决hash冲突,、
  40. * 一旦发生hash冲突,就会将元素放在冲突位置后的最近可用位置),则返回null
  41. */
  42. return null;
  43. }

3.2.3、ThreadLocalMap#remove

  1. /**
  2. * 删除key对应的entry
  3. */
  4. private void remove(ThreadLocal<?> key) {
  5. Entry[] tab = table;
  6. int len = tab.length;
  7. //计算桶位
  8. int i = key.threadLocalHashCode & (len-1);
  9. //从计算出的位置循环查找到entry为null的位置 如果找到entry的key和当前待删除的key相同 在清除该entry
  10. for (Entry e = tab[i];
  11. e != null;
  12. e = tab[i = nextIndex(i, len)]) {
  13. if (e.get() == key) {
  14. //清理当前的entry
  15. e.clear();
  16. //执行一次探测式的清理
  17. expungeStaleEntry(i);
  18. return;
  19. }
  20. }
  21. }

3.3、ThreadLocalMap内部核心方法

3.3.1、ThreadLocalMap#replaceStaleEntry

插入数据时,如果key计算出的位置发现被占用了,则从当前位置向后查找,如果找到entry的key为null的位置,则会调用当前方法替换过期数据。

其中主要逻辑为: 前驱扫描 获取最前面的一个过期数据的下标(不一定最小,是循环访问的),以初始值为key计算出的桶位staleSlot向后迭代,寻找过期数据,直到碰到entry为null结束,获取第一个过期数据的下标位置。如果向后查找过程中,找到了和要查询键值对key相同的entry,证明为重新赋值操作,将键值对放到之前entry失效的位置;如果向后查找过程中并未找到key和待插入key相同的entry,说明当前set操作为添加逻辑,直接将失效数据位置重新赋值。
其中向前和向后遍历时,遇到过期数据会修改slotToExpunge的值,当遇到过期数据时或者向后查询找到了和要查询键值对key相同的entry时,都需要调用
ThreadLocalMap#expungeStaleEntry方法进行一次探测式清理,然后调用ThreadLocalMap#cleanSomeSlots方法执行一次启发式清理。

image.png

  1. /**
  2. * 替换过期Entry(插入数据时,如果key计算出的位置发现被占用了,则从当前位置向后查找,找到一个entry为null的位置
  3. * 或者entry的key为null的位置 或者 entry的key为待插入key的位置,如果entry的key为null的位置,则会调用当前方法替换过期数据)
  4. * @param key 待插入键值对的key
  5. * @param value 待插入键值对的value
  6. * @param staleSlot key计算出的桶位即散列数组的下标
  7. */
  8. private void replaceStaleEntry(ThreadLocal<?> key, Object value,
  9. int staleSlot) {
  10. Entry[] tab = table;
  11. int len = tab.length;
  12. Entry e;
  13. //探测式清理过期数据的下标 初始值为 key计算出的桶位即散列数组的下标staleSlot
  14. int slotToExpunge = staleSlot;
  15. /**
  16. * 前驱扫描 获取最前面的一个过期数据的下标(不一定最小,是循环访问的),赋值给slotToExpunge
  17. * 以初始值为key计算出的桶位staleSlot向前迭代,寻找过期数据,直到碰到entry为null结束
  18. * int i = prevIndex(staleSlot, len); i的初始值为key计算出的桶位的前一个位置
  19. * (e = tab[i]) != null; 循环条件是entry不为null
  20. */
  21. for (int i = prevIndex(staleSlot, len);
  22. (e = tab[i]) != null;
  23. i = prevIndex(i, len)){
  24. //如果当前位置为过期数据,更新探测式清理过期数据的下标为i
  25. if (e.get() == null){
  26. slotToExpunge = i;
  27. }
  28. }
  29. /**
  30. * 以初始值为key计算出的桶位staleSlot向后迭代,寻找过期数据,直到碰到entry为null结束
  31. * 向后扫描 获取第一个过期数据的下标位置赋值给slotToExpunge
  32. */
  33. for (int i = nextIndex(staleSlot, len);
  34. (e = tab[i]) != null;
  35. i = nextIndex(i, len)) {
  36. ThreadLocal<?> k = e.get();
  37. /**
  38. * 如果entry的key和待插入的key一样,说明是替换逻辑
  39. * 直接将
  40. */
  41. if (k == key) {
  42. //更新新的value
  43. e.value = value;
  44. //将过期数据tab[staleSlot] 放到 当前循环到的位置i上
  45. tab[i] = tab[staleSlot];
  46. //将过期数据位置数据tab[staleSlot]保存为更新了的当前entry
  47. tab[staleSlot] = e;
  48. //slotToExpunge == staleSlot 成立说明一开始向前查找过期数据时,并未找到过期的entry,向后扫描时,也未发现过期数据
  49. if (slotToExpunge == staleSlot){
  50. //将探测式清理过期数据的下标修改为当前循环的下标
  51. slotToExpunge = i;
  52. }
  53. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  54. return;
  55. }
  56. /**
  57. * 条件1: k == null 说明遍历的entry是一个过期数据
  58. * 条件2:slotToExpunge == staleSlot说明前驱扫描时未发现过期数据
  59. *
  60. */
  61. if (k == null && slotToExpunge == staleSlot){
  62. //向后遍历找到过期数据了,并且前驱扫描时未发现过期数据,更新探测式清理过期数据的下标为i
  63. slotToExpunge = i;
  64. }
  65. }
  66. /**
  67. * 运行到这里的条件
  68. * 向后查找过程中并未找到key和待插入key相同的entry,说明当前set操作为添加逻辑
  69. */
  70. //将原过期数据的value设置为null
  71. tab[staleSlot].value = null;
  72. //将新插入键值对添加到staleSlot中
  73. tab[staleSlot] = new Entry(key, value);
  74. // slotToExpunge != staleSlot 成立说明 除了staleSlot以外还发现其他过期的数据了
  75. if (slotToExpunge != staleSlot)
  76. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  77. }

3.3.2、ThreadLocalMap#expungeStaleEntry

探测式清理主要是清理了过期的数据以及hash冲突优化
1、从staleSlot这个过期数据位置开始向后查找过期数据,直到对应位置entry为null为止,遍历过程中,如果entry的key为null,则将entry的value设置为null,将当前位置的entry设置为空,size-1
2、遍历过程中对entry的key不为null的数据,需要重新计算桶位,如果是发生hash冲突的,需要将其移到最靠近正确位置的可用位置

image.png

  1. /**
  2. * 探测式的过期数据回收以及hash冲突优化
  3. * 1、从staleSlot这个过期数据位置开始向后查找过期数据,直到对应位置entry为null为止,遍历过程中,如果entry的key为null,
  4. * 则将entry的value设置为null,将当前位置的entry设置为空,size-1
  5. * 2、遍历过程中对entry的key不为null的数据,需要重新计算桶位,如果是发生hash冲突的,需要将其移到最靠近正确位置的可用位置
  6. *
  7. * 当获取元素时发现,有entry的key为null,即散列表有过期数据时需要调用该方法
  8. * @param staleSlot entry的key为null的散列表数组下标
  9. * @return 返回从staleSlot这个过期数据位置开始向后查找最后一个不为空数据的下标
  10. */
  11. private int expungeStaleEntry(int staleSlot) {
  12. Entry[] tab = table;
  13. int len = tab.length;
  14. // 将当前位置的entry的value设置为null
  15. tab[staleSlot].value = null;
  16. //将当前位置的entry设置为空
  17. tab[staleSlot] = null;
  18. //减少一个元素 size-1
  19. size--;
  20. // 当前遍历的entry
  21. Entry e;
  22. //当前遍历的下标
  23. int i;
  24. /**
  25. * i = nextIndex(staleSlot, len) i的初始值为staleSlot的下一个元素位置,因为staleSlot位置已经处理过了
  26. * 循环的条件是 (e = tab[i]) != null 即下一个位置的entry不为null
  27. * i = nextIndex(i, len) 向后循环遍历
  28. *
  29. */
  30. for (i = nextIndex(staleSlot, len);
  31. (e = tab[i]) != null;
  32. i = nextIndex(i, len)) {
  33. //获取当前遍历entry的key
  34. ThreadLocal<?> k = e.get();
  35. //如果key为空,则证明为过期数据,当前entry的key关联的ThreadLocal对象已经被gc回收了,做回收处理
  36. if (k == null) {
  37. // 将entry的value设置为null
  38. e.value = null;
  39. //将当前位置的entry设置为空
  40. tab[i] = null;
  41. //减少一个元素 size-1
  42. size--;
  43. } else {
  44. //key不为空 则证明数据不为空
  45. //根据key的hashcode重新计算下 桶位
  46. int h = k.threadLocalHashCode & (len - 1);
  47. /**
  48. * 如果计算的桶位和当前entry的桶位不同,则证明插入数据时发生了hash冲突,此时需要优化
  49. * 将数据移到离正确桶位更近的位置
  50. */
  51. if (h != i) {
  52. //将当前桶位的数据设置为null
  53. tab[i] = null;
  54. //从entry应该在的正确位置向后查找,循环条件是对应位置的entry不为null,也就是最多查询到 当前循环处理过期数据的位置 i
  55. while (tab[h] != null)
  56. h = nextIndex(h, len);
  57. //将数据放到离数据应该在的正确桶位最靠近的位置
  58. tab[h] = e;
  59. }
  60. }
  61. }
  62. return i;
  63. }


3.3.3、ThreadLocalMap#cleanSomeSlots

启发式清理,返回是否清理了过期数据的标识

image.png

  1. /**
  2. * 启发式清理
  3. * @param i 启发式清理的开始位置
  4. * @param n 当前散列表数组的长度
  5. * @return
  6. */
  7. private boolean cleanSomeSlots(int i, int n) {
  8. //启发式清理是否清除过过期数据
  9. boolean removed = false;
  10. Entry[] tab = table;
  11. int len = tab.length;
  12. do {
  13. //获取下一个位置
  14. i = nextIndex(i, len);
  15. Entry e = tab[i];
  16. //如果当前位置的数据过期了
  17. if (e != null && e.get() == null) {
  18. n = len;
  19. //启发式清理是否清除过过期数据标记为true
  20. removed = true;
  21. //以当前过期的位置做一次探测式清理工作
  22. i = expungeStaleEntry(i);
  23. }
  24. //循环一次 n无符号右移一位
  25. } while ( (n >>>= 1) != 0);
  26. return removed;
  27. }

3.3.4、ThreadLocalMap#cleanSomeSlots

清理所有的过期数据,如果清理后,元素数量还大于散列数组长度的3/4 则调用ThreadLocalMap#resize方法进行扩容

  1. /**
  2. * 清理所有的过期数据,如果清理后,元素数量还大于散列数组长度的3/4 则进行扩容
  3. */
  4. private void rehash() {
  5. //该方法遍历每个桶位进行探测式清理 执行完后当前散列表内所有过期数据都会被移除
  6. expungeStaleEntries();
  7. // 如果清理所有的过期数据后 size还大于散列数组长度的3/4 则进行扩容
  8. if (size >= threshold - threshold / 4)
  9. resize();
  10. }

3.3.5、ThreadLocalMap#resize

每次双倍扩容,创建一个双倍长度的数组循环迁移数据

  1. /**
  2. * 双倍的扩容
  3. */
  4. private void resize() {
  5. Entry[] oldTab = table;
  6. int oldLen = oldTab.length;
  7. int newLen = oldLen * 2;
  8. //创建了一个双倍长度的数组
  9. Entry[] newTab = new Entry[newLen];
  10. int count = 0;
  11. //循环迁移数据
  12. for (Entry e : oldTab) {
  13. if (e != null) {
  14. ThreadLocal<?> k = e.get();
  15. //k == null成立 说明为过期数据, 过期数据不用迁移
  16. if (k == null) {
  17. e.value = null; // Help the GC
  18. } else {
  19. //重新计算新的桶位
  20. int h = k.threadLocalHashCode & (newLen - 1);
  21. //hash时拿到最近的位置
  22. while (newTab[h] != null)
  23. h = nextIndex(h, newLen);
  24. newTab[h] = e;
  25. count++;
  26. }
  27. }
  28. }
  29. //更新扩容阈值
  30. setThreshold(newLen);
  31. size = count;
  32. table = newTab;
  33. }