ThreadLocal源码分析

2019-01-19 21:11:18

线程本地变量,通过为每个线程提供一个变量的副本,从而实现线程安全的访问。
ThreadLocal里有一个叫ThreadLocalMap的内部类,在每个线程对象中都有一个ThreadLocalMap对象,当向一个ThreadLocal对象中存放值的时候,会从Thread对象中获取这个map,如果不存在就创建一个,然后以这个ThreadLocal对象为key,将value存进这个map中,从ThreadLocal中获取对象也是同样是从这个map中取。

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. map.set(this, value);
  6. } else {
  7. createMap(t, value);
  8. }
  9. }
  10. void createMap(Thread t, T firstValue) {
  11. t.threadLocals = new ThreadLocalMap(this, firstValue);
  12. }

所以ThreadLocalMapThreadLocal的核心,与HashMap不同,在hash发生冲突时,这里采用的是线性探测法。


存放键值对的类:
这里使用弱引用来指向ThreadLocal对象是为了当没有强引用指向ThreadLocal对象时,能尽早被回收,如果这里用强引用,那么ThreadLocalMap里总会有一个强引用指向ThreadLocal对象,而ThreadLocalMap对象被Thead对象强引用,这样ThreadLocal对象就和线程的生命周期绑定到一起。采用弱引用后,ThreadLocal对象没用时能被及时回收,但对应的Entry对象却不会,所以ThreadLocalMap中有相应的清理机制,只要Entry.get() == null,这个Entry就可以被清理掉了,在get set remove操作中都会触发不同程度的清理行为。

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. /** The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal<?> k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }

Fields:

  1. // 数组初始容量,必须是2的次方数
  2. private static final int INITIAL_CAPACITY = 16;
  3. // 存放键值对的数组,必要时会扩容,长度必须是2的次方数
  4. private Entry[] table;
  5. // 存放键值对数量
  6. private int size = 0;
  7. // 触发下一次扩容时键值对的数量
  8. private int threshold; // Default to 0

Constructors:

  1. // 第一次向一个空ThreadLocal里放值的时候会用这个构造器来创建
  2. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  3. // 以初始容量创建一个数组
  4. table = new Entry[INITIAL_CAPACITY];
  5. // 采用和HashMap相同的方法计算hash % INITIAL_CAPACITY,所以数组长度必须是2的次方数
  6. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  7. table[i] = new Entry(firstKey, firstValue);
  8. size = 1;
  9. // 计算键值对容量上限
  10. setThreshold(INITIAL_CAPACITY);
  11. }
  12. // 可以看成是负载因子为 2 / 3
  13. private void setThreshold(int len) {
  14. threshold = len * 2 / 3;
  15. }

每个ThreadLocal对象的hashcode会比上一个创建的hashcode0x61c88647,据说这个数和斐波那契数列有关,可以使得hashcode对2的次方数取模的结果分布很均匀,在发生冲突时,可以很快地找到下一个可用的地址。

  1. private final int threadLocalHashCode = nextHashCode();
  2. private static AtomicInteger nextHashCode =
  3. new AtomicInteger();
  4. private static final int HASH_INCREMENT = 0x61c88647;
  5. private static int nextHashCode() {
  6. return nextHashCode.getAndAdd(HASH_INCREMENT);
  7. }

Fields:
getEntry方法,根据key找到键值对:

  1. private Entry getEntry(ThreadLocal<?> key) {
  2. // hash % len
  3. int i = key.threadLocalHashCode & (table.length - 1);
  4. Entry e = table[i];
  5. // 地址没冲突
  6. if (e != null && e.get() == key)
  7. return e;
  8. else
  9. // 地址发生冲突
  10. return getEntryAfterMiss(key, i, e);
  11. }
  12. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  13. Entry[] tab = table;
  14. int len = tab.length;
  15. // 线性地址探测
  16. while (e != null) {
  17. ThreadLocal<?> k = e.get();
  18. if (k == key)
  19. // 找到目标,返回
  20. return e;
  21. if (k == null)
  22. // 该Entry中存的ThreadLocal对象为null
  23. // 说明该Entry中的ThreadLocal对象被回收了,此时需要清理
  24. expungeStaleEntry(i);
  25. else
  26. // 线性探测的下一个下标 (i + 1) % len
  27. i = nextIndex(i, len);
  28. e = tab[i];
  29. }
  30. return null;
  31. }
  32. private static int nextIndex(int i, int len) {
  33. return ((i + 1 < len) ? i + 1 : 0);
  34. }
  35. // 清理失效的弱引用,从staleSlot开始清理一段连续的区域
  36. private int expungeStaleEntry(int staleSlot) {
  37. Entry[] tab = table;
  38. int len = tab.length;
  39. // 该Entry的value置为null,数组此位置置为null
  40. tab[staleSlot].value = null;
  41. tab[staleSlot] = null;
  42. size--;
  43. // Rehash until we encounter null
  44. Entry e;
  45. int i;
  46. for (i = nextIndex(staleSlot, len);
  47. (e = tab[i]) != null;
  48. i = nextIndex(i, len)) {
  49. ThreadLocal<?> k = e.get();
  50. if (k == null) {
  51. // 弱引用失效,需要清理
  52. e.value = null;
  53. tab[i] = null;
  54. size--;
  55. } else {
  56. // 如果该引用未失效,则需要重新计算该Entry的位置
  57. // 否则,如果该Entry前面的位置有被清理过的
  58. // 会造成具有相同hash的Entry在数组中的下标不连续,中间出现null值
  59. // 而在查找的时候,就是以出现null值作为线性探测的结束标志,可能造成明明存在,但就是找不到
  60. int h = k.threadLocalHashCode & (len - 1);
  61. if (h != i) {
  62. while (tab[h] != null)
  63. h = nextIndex(h, len);
  64. tab[h] = e;
  65. }
  66. }
  67. }
  68. return i;
  69. }

set方法:

  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. // 线性探测
  6. for (Entry e = tab[i];
  7. e != null;
  8. e = tab[i = nextIndex(i, len)]) {
  9. ThreadLocal<?> k = e.get();
  10. // 找到对应的Entry
  11. if (k == key) {
  12. e.value = value;
  13. return;
  14. }
  15. // 此位置的Entry失效,可以替换此位置的Entry,将key和value放到到这个位置
  16. if (k == null) {
  17. replaceStaleEntry(key, value, i);
  18. return;
  19. }
  20. }
  21. // 到这里说明这个key是新添加的,之前不存在
  22. tab[i] = new Entry(key, value);
  23. int sz = ++size;
  24. // 清理之后,如果没有元素被移除,且键值对容量达到上限,扩容
  25. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  26. rehash();
  27. }
  28. // 将key value 放在staleSlot处
  29. private void replaceStaleEntry(ThreadLocal<?> key, Object value,
  30. int staleSlot) {
  31. Entry[] tab = table;
  32. int len = tab.length;
  33. Entry e;
  34. // 从staleSlot开始向前查找,找到一片连续的不为null的区间中最左边的失效的弱引用的下标slotToExpunge
  35. int slotToExpunge = staleSlot;
  36. for (int i = prevIndex(staleSlot, len);
  37. (e = tab[i]) != null;
  38. i = prevIndex(i, len))
  39. if (e.get() == null)
  40. slotToExpunge = i;
  41. // 从staleSlot开始向后查找
  42. for (int i = nextIndex(staleSlot, len);
  43. (e = tab[i]) != null;
  44. i = nextIndex(i, len)) {
  45. ThreadLocal<?> k = e.get();
  46. // 在staleSlot后面找到key,将value放在此处
  47. // 然后交换staleSlot和此处的Entry
  48. // 结果是value被放到staleSlot出,i处变为失效引用
  49. if (k == key) {
  50. e.value = value;
  51. tab[i] = tab[staleSlot];
  52. tab[staleSlot] = e;
  53. // 如果staleSlot左边没有失效引用,将清理开始位置设为当前下标
  54. if (slotToExpunge == staleSlot)
  55. slotToExpunge = i;
  56. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  57. return;
  58. }
  59. // 如果staleSlot左边没有失效引用,将清理开始位置设为当前下标
  60. if (k == null && slotToExpunge == staleSlot)
  61. slotToExpunge = i;
  62. }
  63. // 在staleSlot后面没有找到key,说明是新插入的,直接放在这里
  64. tab[staleSlot].value = null;
  65. tab[staleSlot] = new Entry(key, value);
  66. // slotToExpunge != staleSlot 说明在staleSlot左边或右边有失效的弱引用,需要清理
  67. if (slotToExpunge != staleSlot)
  68. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  69. }
  70. // Heuristically scan 启发式扫描
  71. // 传入的i处要么为null,要么是一个有效的Entry,所以从i的下一个位置开始扫描
  72. // n用于控制扫描次数,如果log n 此扫描没有找到失效的引用,扫描就结束了。
  73. private boolean cleanSomeSlots(int i, int n) {
  74. boolean removed = false;
  75. Entry[] tab = table;
  76. int len = tab.length;
  77. do {
  78. i = nextIndex(i, len);
  79. Entry e = tab[i];
  80. // 每次找到失效的弱引用,开始一段连续空间的清理
  81. if (e != null && e.get() == null) {
  82. n = len;
  83. removed = true;
  84. i = expungeStaleEntry(i);
  85. }
  86. } while ( (n >>>= 1) != 0);
  87. return removed;
  88. }
  89. private void rehash() {
  90. // 整个数组清理一遍
  91. expungeStaleEntries();
  92. // 因为做了一次清理,所以size很可能会变小
  93. // 此时size只要达到 len / 2就扩容( 3/4 * 2/3)
  94. if (size >= threshold - threshold / 4)
  95. resize();
  96. }
  97. private void expungeStaleEntries() {
  98. Entry[] tab = table;
  99. int len = tab.length;
  100. for (int j = 0; j < len; j++) {
  101. Entry e = tab[j];
  102. if (e != null && e.get() == null)
  103. expungeStaleEntry(j);
  104. }
  105. }
  106. private void resize() {
  107. Entry[] oldTab = table;
  108. int oldLen = oldTab.length;
  109. // 数组长度变为原来2倍
  110. int newLen = oldLen * 2;
  111. Entry[] newTab = new Entry[newLen];
  112. int count = 0;
  113. // 把旧数组中的Entry重新用线性探测寻找位置,放入新数组
  114. for (Entry e : oldTab) {
  115. if (e != null) {
  116. ThreadLocal<?> k = e.get();
  117. if (k == null) {
  118. e.value = null; // Help the GC
  119. } else {
  120. int h = k.threadLocalHashCode & (newLen - 1);
  121. while (newTab[h] != null)
  122. h = nextIndex(h, newLen);
  123. newTab[h] = e;
  124. count++;
  125. }
  126. }
  127. }
  128. setThreshold(newLen);
  129. size = count;
  130. table = newTab;
  131. }

remove方法:

  1. private void remove(ThreadLocal<?> key) {
  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. // 线性探测找到后,切断弱引用,从当前下标开始清理一段
  9. if (e.get() == key) {
  10. e.clear();
  11. expungeStaleEntry(i);
  12. return;
  13. }
  14. }
  15. }

1
2
0

添加评论

正在回复:
取消
2
1
2
0