ThreadLocal回收后,是怎么获取值的?

王猴卖瓜

共 4098字,需浏览 9分钟

 · 2021-03-14

    ThreadLocal回收后,是怎么获取值的?

  执行逻辑:ThreadLocal#get() ---> setInitialValue() ---> ThreadLocalMap.set(this, value); 。


ThreadLocal在执行set()方法的时候,实际执行set()逻辑的是其内部类ThreadLocalMap。

  1. private void set(ThreadLocal<?> key, Object value) {

  2. Entry[] tab = table;

  3. int len = tab.length;

  4. int i = key.threadLocalHashCode & (len-1);

  5. for (Entry e = tab[i];

  6. e != null;

  7. e = tab[i = nextIndex(i, len)]) {

  8. ThreadLocal<?> k = e.get();

  9. if (k == key) {

  10. e.value = value;

  11. return;

  12. }

  13. if (k == null) {

  14. replaceStaleEntry(key, value, i);

  15. return;

  16. }

  17. }

  18. tab[i] = new Entry(key, value);

  19. int sz = ++size;

  20. if (!cleanSomeSlots(i, sz) && sz >= threshold)

  21. rehash();

  22. }

通过nextIndex()不断获取table上得槽位,直到遇到第一个为null的地方,此处也将是存放具体entry的位置,在线性探测法的不断冲突中,如果遇到非空entry中的key为null,可以表明key的弱引用已经被回收,但是由于线程仍未结束生命周期被回收而导致该entry仍未从table中被回收,那么则会在这里尝试通过replaceStaleEntry()方法,将null key的entry回收掉并set相应的值。

  1. private void replaceStaleEntry(ThreadLocal<?> key, Object value,

  2. int staleSlot) {

  3. Entry[] tab = table;

  4. int len = tab.length;

  5. Entry e;

  6. int slotToExpunge = staleSlot;

  7. for (int i = prevIndex(staleSlot, len);

  8. (e = tab[i]) != null;

  9. i = prevIndex(i, len))

  10. if (e.get() == null)

  11. slotToExpunge = i;

  12. for (int i = nextIndex(staleSlot, len);

  13. (e = tab[i]) != null;

  14. i = nextIndex(i, len)) {

  15. ThreadLocal<?> k = e.get();

  16. if (k == key) {

  17. e.value = value;

  18. tab[i] = tab[staleSlot];

  19. tab[staleSlot] = e;

  20. if (slotToExpunge == staleSlot)

  21. slotToExpunge = i;

  22. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

  23. return;

  24. }

  25. if (k == null && slotToExpunge == staleSlot)

  26. slotToExpunge = i;

  27. }

  28. tab[staleSlot].value = null;

  29. tab[staleSlot] = new Entry(key, value);

  30. if (slotToExpunge != staleSlot)

  31. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

  32. }

在replaceStaleEntry()方法中,带有入参staleSlot为已经得知并准备替换的槽位,该方法重点

一共要完成两件事,找到一趟hash下来第一个出现的nul key位置开始回收,第二件事,将staleSlot位置的null key entry替换为目前想要set的entry,当然该key可能已经在table中,需要将其移动大该位置。

第一个部分,将在目标槽位不断向前扫描直到遇到空槽位,在这一趟扫描下,将记录该位置最早出现的null key位置,并记录为slotToExpunge。

slotToExpunge为接下来清理null key的起始位置槽位,一定是一趟线性探测下来最早出现null key的那个槽位。

在向前扫描完成后,将会从staleSlot开始,向后不断获取下一个位置直到遇到空槽位,期间可能会发生三种情况。

第一种,key存在的其他entry,不管,跳过。

第二种,key不存在的null key entry,如果是首次,更新其slotToExpunge,准备作为接下来回收的起始位置,如果第一部分向前扫描的时候已经存在,将不会修改。

第三种,则是找到了本来将要set的key,也就是该次操作将要把该key从现有的位置swap到staleSlot位置,并回收掉staleSlot原本位置上的null key。

在上述第三种情况未发生的情况下,将会新建一个entry发到staleSlot位置。

在完成staleSlot位置的放入后,将会通过expungeStaleEntry()方法从该趟hash中遇到的第一个null key的位置回收,并在之后停过cleanSomeSlots()方法进行一次启发式的回收。

 

先看expungeStaleEntry()方法。

  1. private int expungeStaleEntry(int staleSlot) {

  2. Entry[] tab = table;

  3. int len = tab.length;

  4. tab[staleSlot].value = null;

  5. tab[staleSlot] = null;

  6. size--;

  7. Entry e;

  8. int i;

  9. for (i = nextIndex(staleSlot, len);

  10. (e = tab[i]) != null;

  11. i = nextIndex(i, len)) {

  12. ThreadLocal<?> k = e.get();

  13. if (k == null) {

  14. e.value = null;

  15. tab[i] = null;

  16. size--;

  17. } else {

  18. int h = k.threadLocalHashCode & (len - 1);

  19. if (h != i) {

  20. tab[i] = null;

  21. while (tab[h] != null)

  22. h = nextIndex(h, len);

  23. tab[h] = e;

  24. }

  25. }

  26. }

  27. return i;

  28. }

这个方法主要做了两件事,一件事是从hash下某个值的一轮线性探测的首个null key空键开始向后不断回收,第二件事就是在这趟中的正常键将会进行rehash以便将其移至前序被回收的空槽位上。

 

在完成一趟hash下的空键回收和rehash之后,将会再次通过cleanSomeSlots()方法进行一次启发式的扫描回收。

  1. private boolean cleanSomeSlots(int i, int n) {

  2. boolean removed = false;

  3. Entry[] tab = table;

  4. int len = tab.length;

  5. do {

  6. i = nextIndex(i, len);

  7. Entry e = tab[i];

  8. if (e != null && e.get() == null) {

  9. n = len;

  10. removed = true;

  11. i = expungeStaleEntry(i);

  12. }

  13. } while ( (n >>>= 1) != 0);

  14. return removed;

  15. }

此处的遍历,将会在前一个方法中的最后一个key的下一个hash位置进行扫描并进行垃圾回收,理想情况下的执行次数是log2(散列表长度)次,当遇到null key空键时将会重置扫描次数,这里的次数实现选择是介于不扫描和全表遍历之间的一种选择,尽可能在保证效率的前提下所往后进行扫描。

 

在上述的整个流程走完后,回顾下最一开始的set()方法最后。

  1. if (!cleanSomeSlots(i, sz) && sz >= threshold)

  2. rehash();

当最后散列表中的个数大于负载个数后,将会调用rehash()方法进行扩容rehash分配。

  1. private void rehash() {

  2. expungeStaleEntries();

  3. // Use lower threshold for doubling to avoid hysteresis

  4. if (size >= threshold - threshold / 4)

  5. resize();

  6. }

在rehash()方法中,共分为两步expungeStaleEntries()方法将对散列表中的所有槽位进行一次遍历,对所有空键调用上述的expungeStaleEntryEntry()方法进行回收。

在回收之后,再次判断是否需要扩容,如果仍旧个数大于负载数量,将会resize(),将会通过resize()进行扩容,重新根据新的长度进行一次散列表的重分配。


浏览 43
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报