该类主要用于不同线程存储自己的线程本地变量。本文先通过一个示例简单介绍该类的使用方法,然后从ThreadLocal类的初始化、存储结构、增删数据和hash值计算等几个方面,分析对应源码。采用的版本为jdk1.8。

ThreadLocal对象可以在多个线程中被使用,通过set()方法设置线程本地变量,通过get()方法获取设置的线程本地变量。我们先通过一个示例简单了解下使用方法:

  1. public static void main(String[] args){
  2. ThreadLocal<String> threadLocal = new ThreadLocal<>();
  3. // 线程1
  4. new Thread(()->{
  5. // 查看是否有初始值
  6. System.out.println("线程1的初始值:"+threadLocal.get());
  7. // 设置线程1的值
  8. threadLocal.set("V1");
  9. // 输出
  10. System.out.println("线程1的值:"+threadLocal.get());
  11. // 等待一段时间,等线程2设置值后再查看线程1的值
  12. try {
  13. Thread.sleep(1000);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println("线程1的值:"+threadLocal.get());
  18. }).start();
  19. // 线程2
  20. new Thread(()->{
  21. // 等待线程1设置初始值
  22. try {
  23. Thread.sleep(500);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. // 查看线程2的初始值
  28. System.out.println("线程2的值:"+threadLocal.get());
  29. // 设置线程2的值
  30. threadLocal.set("V2");
  31. // 查看线程2的值
  32. System.out.println("线程2的值:"+threadLocal.get());
  33. }).start();
  34. }

由于threadlocal设置的值是在每个线程中都有一个副本的,线程之间不会互相影响。代码运行的结果如下所示:

  1. 线程1的初始值:null
  2. 线程1的值:V1
  3. 线程2的值:null
  4. 线程2的值:V2
  5. 线程1的值:V1

ThreadLocal类只有一个无参的构造方法,如下所示:

  1. /**
  2. * Creates a thread local variable.
  3. * @see #withInitial(java.util.function.Supplier)
  4. */
  5. public ThreadLocal() {
  6. }

但其实还有一个带参数的构造方法,不过是它的子类。ThreadLocal中定义了一个内部类SuppliedThreadLocal,为继承自ThreadLocal类的子类。可以通过该类进行给定初始值的初始化,其定义如下:

  1. static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
  2. private final Supplier<? extends T> supplier;
  3. SuppliedThreadLocal(Supplier<? extends T> supplier) {
  4. this.supplier = Objects.requireNonNull(supplier);
  5. }
  6. @Override
  7. protected T initialValue() {
  8. return supplier.get();
  9. }
  10. }

通过TheadLocal threadLocal = Thread.withInitial(supplier);这样的语句可以进行给定初始值的初始化。在某个线程第一次调用get()方法时,会执行initialValue()方法设置线程变量为传入supplier中的值。

  1. public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
  2. return new SuppliedThreadLocal<>(supplier);
  3. }

在jdk1.8版本中,使用的是TheadLocalMap这一个容器存储线程本地变量。

该容器的设计思想和HashMap有很多共同之处。比如:内部定义了Entry节点存储键值对(使用ThreadLocal对象作为键);使用一个数组存储entry节点;设定一个阈值,超过阈值时进行扩容;通过键的hash值与数组长度进行&操作确定下标索引等。但也有很多不同之处,具体我们在后续介绍ThreadLocalMap类时再详细分析。

  1. static class ThreadLocalMap {
  2. // Entry节点定义
  3. static class Entry extends WeakReference<ThreadLocal<?>> {
  4. /** The value associated with this ThreadLocal. */
  5. Object value;
  6. Entry(ThreadLocal<?> k, Object v) {
  7. super(k);
  8. value = v;
  9. }
  10. }
  11. // 存储元素的数组
  12. private Entry[] table;
  13. // 容器内元素数量
  14. private int size = 0;
  15. // 阈值
  16. private int threshold; // Default to 0
  17. // 修改和添加元素
  18. private void set(ThreadLocal<?> key, Object value){
  19. ...
  20. }
  21. // 移除元素
  22. private void remove(ThreadLocal<?> key) {
  23. ...
  24. }
  25. ...
  26. }

ThreadLocal类提供了get(),set()和remove()方法来操作当前线程的threadlocal变量副本。底层则是基于ThreadLocalMap容器来实现数据操作。

不过要注意的是:ThreadLocal中并没有ThreadLocalMap的成员变量,ThreadLocalMap对象是Thread类中的一个成员,所以需要通过通过当前线程的Thread对象去获取该容器。

每一个线程Thread对象都会有一个map容器,该容器会随着线程的终结而回收。

设置线程本地变量的方法。

  1. public void set(T value) {
  2. // 获取当前线程对应的Thread对象,其是map键值对中的健
  3. Thread t = Thread.currentThread();
  4. // 获取当前线程对象的容器map
  5. ThreadLocalMap map = getMap(t);
  6. // 如果容器不为null,则直接设置元素。否则用线程对象t和value去初始化容器对象
  7. if (map != null)
  8. map.set(this, value);
  9. else
  10. createMap(t, value);
  11. }
  12. // 通过当前线程的线程对象获取容器
  13. ThreadLocalMap getMap(Thread t) {
  14. return t.threadLocals;
  15. }
  16. // 创建map容器,本质是初始化Thread对象的成员变量threadLocals
  17. void createMap(Thread t, T firstValue) {
  18. t.threadLocals = new ThreadLocalMap(this, firstValue);
  19. }

获取线程本地变量的方法。

  1. public T get() {
  2. // 获取当前线程对象
  3. Thread t = Thread.currentThread();
  4. // 获取当前线程对象的容器map
  5. ThreadLocalMap map = getMap(t);
  6. if (map != null) {
  7. ThreadLocalMap.Entry e = map.getEntry(this);
  8. // 如果容器不为null且容器内有当前threadlocal对象对应的值,则返回该值
  9. if (e != null) {
  10. @SuppressWarnings("unchecked")
  11. T result = (T)e.value;
  12. return result;
  13. }
  14. }
  15. // 如果容器为null或者容器内没有当前threadlocal对象绑定的值,则先设置初始值并返回该初始值
  16. return setInitialValue();
  17. }
  18. // 设置初始值。主要分为两步:1.加载和获取初始值;2.在容器中设置该初始值。
  19. // 第二步其实和set(value)方法实现一模一样。
  20. private T setInitialValue() {
  21. // 加载并获取初始值,默认是null。如果是带参初始化的子类SuppliedThreadLocal,会有一个输入初始值。
  22. // 当然也可以继承ThreadLocal类重写该方法设置初始值
  23. T value = initialValue();
  24. Thread t = Thread.currentThread();
  25. ThreadLocalMap map = getMap(t);
  26. // 如果容器不为null,则直接设置元素。否则用线程对象t和value去初始化容器对象
  27. if (map != null)
  28. map.set(this, value);
  29. else
  30. createMap(t, value);
  31. return value;
  32. }

移除线程本地变量的方法

  1. public void remove() {
  2. // 如果容器不为null就调用容器的移除方法,移除和该threadlocal绑定的变量
  3. ThreadLocalMap m = getMap(Thread.currentThread());
  4. if (m != null)
  5. m.remove(this);
  6. }

ThreadLocal的hash值用于ThreadLocalMap容器计算数组下标。类中定义threadLocalHashCode表示其hash值。类中定义了静态方法和静态原子变量计算hash值,也就是说所有的threadLocal对象共用一个增长器。

  1. // 当前ThreadLocal对象的hash值
  2. private final int threadLocalHashCode = nextHashCode();
  3. // 用来计算hash值的原子变量,所有的threadlocal对象共用一个增长器
  4. private static AtomicInteger nextHashCode = new AtomicInteger();
  5. // 魔法数字,使hash散列均匀
  6. private static final int HASH_INCREMENT = 0x61c88647;
  7. // 计算hash值的静态方法
  8. private static int nextHashCode() {
  9. return nextHashCode.getAndAdd(HASH_INCREMENT);
  10. }

我们使用同样的方法定义一个测试类,定义多个不同测试类对象,看看hash值的生成情况。如下所示,可以看到hash值都不同,是共用的一个增长器。

  1. public class Test{
  2. private static final int HASH_INCREMENT = 0x61c88647;
  3. public static AtomicInteger nextHashCode = new AtomicInteger();
  4. public final int nextHashCode = nextHashCode();
  5. private static int nextHashCode() {
  6. return nextHashCode.getAndAdd(HASH_INCREMENT);
  7. }
  8. public static void main(String[] args){
  9. for (int i = 0; i < 5; i++) {
  10. Test test = new Test();
  11. System.out.println(test.nextHashCode);
  12. }
  13. }
  14. // 输出的hash值
  15. 0
  16. 1640531527
  17. -1013904242
  18. 626627285
  19. -2027808484
  20. }

ThreadLocalMap类是ThreadLocal的内部类。其作为一个容器,为ThreadLocal提供操作线程本地变量的功能。每一个Thread对象中都会有一个ThreadLocalMap对象实例(成员变量threadLocals,初始值为null)。因为map是Thread对象的非公共成员,不会被并发调用,所以不用考虑并发风险。

后文将从数据存储设计、初始化、增删数据等方面分析对应源码。

该map和hashmap类似,使用一个Entry数组来存储节点元素,定义size变量表示当前容器中元素的数量,定义threshold变量用于计算扩容的阈值。

  1. // Entry数组
  2. private Entry[] table;
  3. // 容器内元素个数
  4. private int size = 0;
  5. // 扩容计算用阈值
  6. private int threshold;

不同的是Entry节点为WeakReference类的子类,使用引用字段作为键,将弱引用字段(通常是ThreadLocal对象)和值绑定在一起。使用弱引用是为了使得threadLocal对象可以被回收,(如果将key作为entry的一个成员变量,那线程销毁前,threadLocal对象不会被回收掉,即使该threadLocal对象不再使用)。

  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. }

提供了带初始键和初始值的map构造方法,还有一个基于已有map的构造方法(用于ThreadLocal的子类InheritableThreadLocal初始化map容器,目的是将父线程的map传入子线程,会在创建子线程的过程中自动执行)。如下所示:

  1. // 基于初始键值的构造函数
  2. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  3. // 基于输入键值构建节点
  4. table = new Entry[INITIAL_CAPACITY];
  5. // 根据键的hash值计算所在数组下标
  6. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  7. // 采用懒加载的方式,只创建一个必要的节点
  8. table[i] = new Entry(firstKey, firstValue);
  9. size = 1;
  10. // 设置阈值为初始长度的2/3,初始长度默认为12,那么阈值为为8
  11. setThreshold(INITIAL_CAPACITY);
  12. }
  13. // 基于已有map的构造函数
  14. private ThreadLocalMap(ThreadLocalMap parentMap) {
  15. // 获取传入map的节点数组
  16. Entry[] parentTable = parentMap.table;
  17. int len = parentTable.length;
  18. setThreshold(len);
  19. // 构造相同长度的数组
  20. table = new Entry[len];
  21. // 深拷贝传入数组中各个节点到当前容器数组
  22. // 注意这里因为采用开放地址解决hash冲突,拷贝后的元素在数组中的位置与原数组不一定相同
  23. for (int j = 0; j < len; j++) {
  24. // 获取数组各个位置上的节点
  25. Entry e = parentTable[j];
  26. if (e != null) {
  27. @SuppressWarnings("unchecked")
  28. ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
  29. if (key != null) {
  30. //确保key为InheritableThreadLocal类型,否则抛出UnsupportedOperationException
  31. Object value = key.childValue(e.value);
  32. Entry c = new Entry(key, value);
  33. // 根据hash值和数组长度,计算下标
  34. int h = key.threadLocalHashCode & (len - 1);
  35. // 这里采用开放地址的方法解决hash冲突
  36. // 当发生冲突时,就顺延到数组下一位,直到该位置没有元素
  37. while (table[h] != null)
  38. h = nextIndex(h, len);
  39. table[h] = c;
  40. size++;
  41. }
  42. }
  43. }
  44. }

这里将移除元素的方法放在前面,是因为其它部分会频繁使用过时节点的移除方法。先理解这部分内容有助于后续理解其他部分。

根据key移除容器元素的方法:

  1. private void remove(ThreadLocal<?> key) {
  2. // 计算索引下标
  3. Entry[] tab = table;
  4. int len = tab.length;
  5. int i = key.threadLocalHashCode & (len-1);
  6. // 从下标i处开始向后寻找是否有key对应节点,直到遇到Null节点
  7. for (Entry e = tab[i];
  8. e != null;
  9. e = tab[i = nextIndex(i, len)]) {
  10. // 如果遇到key对应节点,执行移除操作
  11. if (e.get() == key) {
  12. // 移除节点的键(弱引用)
  13. e.clear();
  14. // 移除该过时节点
  15. expungeStaleEntry(i);
  16. return;
  17. }
  18. }
  19. }

移除过时节点的执行方法:
移除过时节点除了将该节点置为null之外,还要对该节点之后的节点进行移动,看看能不能往前找合适的空格转移。

这种方法有点类似jvm垃圾回收算法的标记-整理方法。都是将垃圾清除之后,将剩余元素进行整理,变得更紧凑。这里的整理是需要强制执行的,目的是为了保证开放地址法一定能在连续的非null节点块中找到已有节点。(试想,如果把过时节点移除而不整理,该节点为null,将前后节点分开了。而如果后面有某个节点hash计算的下标在前面的节点块,在查找节点时通过开放地址会找不到该节点)。示意图如下:

  1. private int expungeStaleEntry(int staleSlot) {
  2. // 获取entyy数组和长度
  3. Entry[] tab = table;
  4. int len = tab.length;
  5. // 清除staltSlot节点的值的引用,清除节点的引用
  6. tab[staleSlot].value = null;
  7. tab[staleSlot] = null;
  8. // 容器元素个数-1
  9. size--;
  10. // 清除staleSlot节点后的整理工作
  11. // 将staleSlot索引后的节点计算下标往前插空移动
  12. Entry e;
  13. int i;
  14. // 遍历连续的非null节点,直到遇到null节点
  15. for (i = nextIndex(staleSlot, len);
  16. (e = tab[i]) != null;
  17. i = nextIndex(i, len)) {
  18. ThreadLocal<?> k = e.get();
  19. // case1:如果遍历到的节点是过时节点,将该节点清除,容器元素数量-1
  20. if (k == null) {
  21. e.value = null;
  22. tab[i] = null;
  23. size--;
  24. } else {
  25. // case2:如果遍历到的节点不是过时节点,重新计算下标
  26. int h = k.threadLocalHashCode & (len - 1);
  27. // 当下标不是当前位置时,从hash值计算的下标h处,开放地址往后顺延插空
  28. if (h != i) {
  29. // 先将该节点置为null
  30. tab[i] = null;
  31. // 找到为null的节点,插入节点
  32. while (tab[h] != null)
  33. h = nextIndex(h, len);
  34. tab[h] = e;
  35. }
  36. }
  37. }
  38. return i;
  39. }

移除所有过时节点的方法:很简单,全局遍历,移除所有过时节点。

  1. private void expungeStaleEntries() {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. for (int j = 0; j < len; j++) {
  5. Entry e = tab[j];
  6. // 遇到过时节点,清除整理该节点所在的连续块
  7. if (e != null && e.get() == null)
  8. expungeStaleEntry(j);
  9. }
  10. }

尝试去扫描一些过时节点并清除节点,如果有节点被清除会返回true。这里只执行了logn次扫描判断,是为了在不扫描和全局扫描之间找到一种平衡,是上面的方法的一个平衡。

  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. // 遇到过时节点,清除整理该节点所在连续块
  9. if (e != null && e.get() == null) {
  10. n = len;
  11. removed = true;
  12. // 从该连续块后第一个null节点开始
  13. i = expungeStaleEntry(i);
  14. }
  15. } while ( (n >>>= 1) != 0);
  16. return removed;
  17. }

获取容器元素的方法:

  1. // 根据key快速查找entry节点
  2. private Entry getEntry(ThreadLocal<?> key) {
  3. // 通过threadLocal对象(key)的hash值计算数组下标
  4. int i = key.threadLocalHashCode & (table.length - 1);
  5. // 取对应下标元素
  6. Entry e = table[i];
  7. if (e != null && e.get() == key)
  8. return e;
  9. else
  10. // 查找不到有两种情况:
  11. // 1.对应下标桶位为空
  12. // 2对应下标桶位元素不是key关联的entry(开放地址解决hash冲突导致的)
  13. return getEntryAfterMiss(key, i, e);
  14. }
  15. // 初次查找失败后再次查找entry节点
  16. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  17. // 获取entry数组及长度
  18. Entry[] tab = table;
  19. int len = tab.length;
  20. // 如果e为null,说明对应下标桶位为空,找不到key对应的entry
  21. // 如果e不为null,则用解决hash冲突时的方法(顺延数组下一位)一直找下去,直到找到或e为null
  22. while (e != null) {
  23. ThreadLocal<?> k = e.get();
  24. if (k == key)
  25. return e;
  26. // 在寻找的过程中如果节点的key,即ThreadLocal已经被回收(被弱引用的对象可能会被回收)
  27. // 则移除过时的节点,移除过时节点的方法分析见移除元素部分
  28. if (k == null)
  29. expungeStaleEntry(i);
  30. else
  31. i = nextIndex(i, len);
  32. e = tab[i];
  33. }
  34. // 没有找到,返回null
  35. return null;
  36. }

增加和修改容器元素的方法:
这里在根据hash值计算出下标后,由于是开放地址解决hash冲突,会顺序向后遍历直到遇到null或遇到key对应的节点。

这里会出现三种情况:

case1:遍历时找到了key对应节点,这时直接修改节点的值即可;

case2:遍历中遇到了有过时的节点(key被回收的节点);

case3:遍历没有遇到过时的节点,也没有找到key对应节点,说明此时应该插入新节点(用输入键值构造新节点)。因为是增加新元素,所以可以容量会超过阈值。在删除节点后容量如果超过阈值,则要进行扩容操作。

  1. private void set(ThreadLocal<?> key, Object value) {
  2. // 获取数组,计算key对应的数组下标
  3. Entry[] tab = table;
  4. int len = tab.length;
  5. int i = key.threadLocalHashCode & (len-1);
  6. // 从下标i开始,顺序遍历数组(顺着hash冲突开放地址的路径),直到节点为null
  7. for (Entry e = tab[i];
  8. e != null;
  9. e = tab[i = nextIndex(i, len)]) {
  10. // 获取遍历到的节点的key
  11. ThreadLocal<?> k = e.get();
  12. // case1:命中key,说明已存在key对应节点,修改value值即可
  13. if (k == key) {
  14. e.value = value;
  15. return;
  16. }
  17. // case2:如果遍历到的节点的key为null,说明该threadLocal对象已经被回收
  18. if (k == null) {
  19. replaceStaleEntry(key, value, i);
  20. return;
  21. }
  22. }
  23. // case3:遍历节点直到null都没有找到对应key,说明map中没有key对应entry
  24. // 则在该位置用输入键和值新建一个entry节点
  25. tab[i] = new Entry(key, value);
  26. int sz = ++size;
  27. // 判断是否清理过时节点后,在判断是否需要扩容
  28. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  29. rehash();
  30. }

case2:增加和修改过程中遇到已经过时的节点的处理。这里的参数staleSlot表示key计算的下标开始往后遇到的第一个过时节点,不管map中有无key对应的节点,该位置之后一定会存入key的节点。这里定义了一个变量slotToExpunge,其含义是左右连续非null的entry块中第一个过时节点(记录该位置是为了后续清除过时节点可以从slotToExpunge处开始)。示意如下:

这步操作有两种情况:

casse2.1:从过时节点staleSlot往后查找遇到key对应节点,则将staleSlot处节点与key对应节点交换。然后清除整理连续块。

casse2.2:没遇到key对应节点,说明map中不存在key对应节点,则新建一个节点填入staleSlot处。然后清除整理连续块。

  1. private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
  2. // 获取entry数组和长度
  3. Entry[] tab = table;
  4. int len = tab.length;
  5. Entry e;
  6. // 往前移动寻找第一个过时节点(直到遇到null),如果没找到的话说明第一个过时节点为staleslot处节点
  7. // slotToExpunge表示连续块中第一个过时节点
  8. int slotToExpunge = staleSlot;
  9. for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
  10. if (e.get() == null)
  11. slotToExpunge = i;
  12. // 从输入下标staleSlot向后找到第一个出现的key对应的节点或过时的节点(key被回收的节点)
  13. for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
  14. ThreadLocal<?> k = e.get();
  15. // case2.1:如果找到key对应的节点,则用staleSlot处节点和该节点交换,以保持hash表的顺序(hash冲突时顺序向后寻找)
  16. // 交换后的staleSlot节点及其之前的过时节点会被清除
  17. if (k == key) {
  18. // 交换staleSlot处节点和key对应节点
  19. e.value = value;
  20. tab[i] = tab[staleSlot];
  21. tab[staleSlot] = e;
  22. // 更新slotToExpunge的值,使其保持连续块中第一个过时节点的特性,方便后续清理过时节点。
  23. if (slotToExpunge == staleSlot)
  24. slotToExpunge = i;
  25. // 从slotToExpunge开始清除整理连续块
  26. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  27. return;
  28. }
  29. // 如果遇到过时节点,更新slotToExpunge的值
  30. if (k == null && slotToExpunge == staleSlot)
  31. slotToExpunge = i;
  32. }
  33. // case2.2:没有找到key对应节点,增加新节点并填入staleSlot处
  34. tab[staleSlot].value = null;
  35. tab[staleSlot] = new Entry(key, value);
  36. // 这里如果slotToExpunge=staleSlot,说明连续块中只有一个过时节点,且已经被新建节点填入,就不需要再整理。
  37. // 如果除了原staleSlot处,还有其它过时节点,从slotToExpunge开始清除整理连续块
  38. if (slotToExpunge != staleSlot)
  39. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  40. }

case3:增加元素后可能超过阈值导致的扩容处理

  1. private void rehash() {
  2. // 清除所有过时节点
  3. expungeStaleEntries();
  4. // 在清除所有过时节点后,如果数量超过3/4的阈值,则进行扩容处理
  5. // setThreshold()方法非公有,threshold值一直为数组长度的2/3,所以这里是超过数组长度一半就进行扩容
  6. if (size >= threshold - threshold / 4)
  7. resize();
  8. }
  9. /**
  10. * 双倍扩容
  11. */
  12. private void resize() {
  13. // 获取旧数组和长度
  14. Entry[] oldTab = table;
  15. int oldLen = oldTab.length;
  16. // 新数组长度为原来的两倍
  17. int newLen = oldLen * 2;
  18. Entry[] newTab = new Entry[newLen];
  19. int count = 0;
  20. // 遍历原数组元素
  21. for (int j = 0; j < oldLen; ++j) {
  22. Entry e = oldTab[j];
  23. // 如果为非null节点
  24. if (e != null) {
  25. ThreadLocal<?> k = e.get();
  26. // 如果是过时节点,则将value置为null,可以使得value的实体尽快被回收
  27. if (k == null) {
  28. e.value = null; // Help the GC
  29. } else {
  30. // 如果是正常节点,计算下标,重新填入新数组(开放地址解决hash冲突)
  31. int h = k.threadLocalHashCode & (newLen - 1);
  32. while (newTab[h] != null)
  33. h = nextIndex(h, newLen);
  34. newTab[h] = e;
  35. // 新数组元素个数+1
  36. count++;
  37. }
  38. }
  39. }
  40. // 重新设置阈值
  41. setThreshold(newLen);
  42. size = count;
  43. // 将变量table指向新数组
  44. table = newTab;
  45. }

先来聊一聊内存泄漏这个概念。我的理解是有一块内存空间,如果不再被使用但又不能被垃圾回收器回收掉,那么就相当于这块内存少了这块空间,即出现了内存泄露问题。如果内存泄露的空间一直在积累,那么最终会导致可用空间一直减少,最终可能导致程序无法运行。

ThreadLocalMap中也是有可能会出现该问题的,map中entry节点的key为弱引用,如果key没有其它强引用,是会被垃圾收集器回收的。回收之后,map中该节点的value就不会再被使用,但value又被entry节点强引用,不会被回收。这就相当于value这块内存空间发生了泄露。所以能看到在源码中很多方法都进行了清除过时节点的操作,为的就是尽量避免内存泄漏。

在看源码时,一直在思考为什么entry节点的键要采用弱引用的方式。不妨反过来思考,如果entry节点将threadLocal对象作为一个成员变量,而不是采用弱引用的方式,那么entry节点一直对key和value保持着强引用关系,即使threadlocal对象在其它地方都不再使用,该对象也不会被回收。这就会导致entry节点永远不会被回收(只要线程不终结),而且也不能主动去判断是否切断map中threadlocal对象的引用(不知道是否还有其它地方引用到了)。

因为map是Thread对象的一个成员变量,线程不终结,map是不会被回收的,如果发生了内存泄露的问题,可能会一直积累下去,最终导致程序发生异常。而key采用弱引用加之主动的判断过时节点(判断是否过时很简单,看key是否为null即可)并进行清除处理可以最大限度的减少内存泄露的发生。

版权声明:本文为ningbing原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/ningbing/p/14841726.html