ThreadLocal 怎么就内存溢出了,看源码究竟是怎么写的。

ThreadLocal 怎么就内存溢出了,看源码究竟是怎么写的。,第1张

ThreadLocal 怎么就内存溢出了,看源码究竟是怎么写的。

ThreadLocal 对于程序员来说,一点也不陌生,为啥?

平常用过,面试也问过。

可 ThreadLocal 也还是有点神秘,内部原理是啥,估计能说清的也没几个。

就比如,ThreadLocal 有 内存溢出 的风险,网上搜索下,大把文章这么说?

我就纳闷了,ThreadLocal 如此优秀,为毛老拿 OOM 说事儿,溢出个毛线呀!

鄙人斗胆,解读下ThreadLocal 的源码(JDK1.8),说说所谓的 “内存溢出”。

如果你对 ThreadLocal 有相当的了解,直接从第四开始看

一、ThreadLocal 是什么

先看官方解释

 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 * 

大概意思是说:
ThreadLocal 是 线程局部变量。不同于全局变量,这些局部变量是线程私有,并且是各自初始化的变量副本。 ThreadLocal 实例提供静态属性,与 thread 进行关联。

这是英文,翻译过来,不接地气,显得有点别扭。

通俗点解释,线程(thread)有很多属性,比如说线程的名字。

A线程,可以通过 getName() 这个方法,可以得到 A 线程自己的线程名。

B线程,也可以通过 getName() 这个方法,拿到自己的线程名。

这里的线程名就是 线程(thread) 的内存的一个属性,不会有多线程竞争的问题。

线程(thread) 除了 线程名 这个属性之外,还有很多其它的属性,其中有个属性,是一个 Map 类型。

ThreadLocal 用来 *** 作这个 Map,比如 在Map中设置一个值,查找一个值,给 Map 扩容等等。

与线程名一样,这个 Map,不会存在线程竞争问题。

这点我说的啰嗦一点,网上ThreadLocal 的相关文章,很少有说解释ThreadLocal 是什么。

线程高并发时,有时候也会说到 ThreadLocal,但它和高并发没啥关系。

先记住这一句:ThreadLocal 是用来解决,线程间数据隔离问题的。

再打个比方:中国特色的春运抢票,比如说十万个人抢1000张票。

抢票就是一个高并发问题。这里十万个人抢票,大致可理解为 十万个 线程。

1000 张票,是抢夺的对象,谁抢到是谁的,抢到就可以坐高铁回家。

抢票的每个人,都会有个身份z,可以对应理解为线程名,

每个人抢到票,都是要支付的,得有个钱包吧。钱包可以理解为上文说的 thread的那个 Map.

对钱包进行钱的转入转出 *** 作,相当于ThreadLocal ,它封装了相应的方法, *** 作 那个Map。

高并发和 ThreadLocal 有没有啥关系?说有也行,说没有也行。自己瞧着办吧!

上文中,对 ThreadLocal 的官方解释,还有一句

(e.g., a user ID or Transaction ID)

这个说的是 ThreadLocal 的应用场景,比如用户登录后的 userId , 某方法的 事务ID。

我不知道该怎么解释了,好好琢磨下抢高铁票那个例子吧。

二、ThreadLocal 线程安全么?会引起内存溢出么?

ThreadLocal *** 作的是 线程的内部属性,不可能有线程间的竞争,也不没有所谓线程安全不安全的事儿了。

再强调一次:ThreadLocal 是用来解决,线程间数据隔离问题的。

Threadlocal 会引起内存溢出么?可以放心使用么?

这个问题先放一放,我问你,平常手机充电,你用的插座安全吗?

估计你会说:都在市场上卖了这么多年,全球几亿人用了百十年了,当然是安全的呀!

那我再问你,那朝着插座撒泡尿,安全?把圆规的两个尖儿插到插座的两个插孔里,安全?

我很负责任的警告你:

朝着插座撒尿,警察蜀黍不会因为有伤风化,把你带到小黑屋。

然而, 马克思会亲自接见你,真的!

把圆规尖扎到插座孔里,你能直接见耶稣!!!

插座是安全的,前提是你正常使用它。你硬要非主流使用它,耶稣也救不了你。

同样,Threadlocal 正常不会引起内存溢出。你非要zuo,那溢出了怪谁。

到底怎样,ThreadLocal 才会引起内存溢出呢?

前面说过,Thread 的有个内存的属性,类型是 个 Map, ThreadLocal 是 *** 作这个 Map的。

这个 Map 的 Key 是 ThreadLocal 实例的内存地址,value 是要存入的值。

是 Map 就会有个容量的极限值,超过这个值,那可不就把 Map 给撑破了。

说白了就是一直往这个Map里塞值,直到塞爆它,我写了个 demo

    public static void main(String[] args) {
        int max = Integer.MAX_VALUE;
        for(int i = 0; i < max; i++){
            ThreadLocal threadLocal = new ThreadLocal();
            threadLocal.set("测试thread属性" + i);
            if(i % 1000 == 0){
                System.out.println("set 中 " + i);
            }
        }
        System.out.println("测试结束");
        Thread thread = Thread.currentThread();
    }

执行上面那段代码后,我去吃午饭,想着让它慢慢跑

吃了快一个小时,回来代码仍旧在执行中,并没有到 OOM 的程度。

这就是很 zuo 死的代码,玩儿了命的往 Map 里塞东西,即便这样,执行了几十分钟,没有内存溢出。

俗话又说,饭前便后要洗手,即便你没洗手,也不是一定会生病。

上面那段很 zuo 的代码,就相当于没洗手, 应该加一句 threadLocal.remove();

这样,即便循环改为 long 的最大值,执行到天荒地老,也不会 OOM。(为什么,后面分析源码再细说

ThreadLocal 放心大胆的用,没有问题。你创建的 ThreadLocal 实例,能有几个,不会把 Map 撑破。

那 ThreadLocal 到底会不会引起内存溢出?

那你先说插座安全不安全?

你要说插座安全,那 ThreadLocal 就不会引起内存溢出;

你要说插座不安全,那 ThreadLocal 会引起内存溢出。

这样的回答,您还满意不???

三、ThreadLocal 的使用

前面的例子中说过,Thread 中有个属性是 Map 类型, ThreadLocal 就是 *** 作这个Map

public class Thread implements Runnable {

    private volatile String name;
    
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
     ……
}

这是 Thread 类 摘录的两行代码,threadLocals 就是刚刚说的那个 Map,

它是定义在 ThreadLocal 中的一个内部类。然后我们来看 ThreadLocal 的使用

  public static void main(String[] args) {
     ThreadLocal threadLocal = new ThreadLocal();
     threadLocal.set("测试thread属性");
     String value = threadLocal.get();
     threadLocal.remove();
     Thread thread = Thread.currentThread();
  }

这个很简单一个示例,创建一个 ThreadLocal 的实例,设置一个值。看下 set 源码。

  public void set(T value) {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }

  ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
  }

  void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
  }

代码很简洁,getMap(t) 这个方法,就是拿到 Thread 类中的属性 threadLocals,就是那个 Map。

  • Map 存在,设置 key, value。
  • Map 不存在,创建Map,然后再设置 key, value。

再看 threadLocal.get() 的源码

  public T get() {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
          }
      }
      return setInitialValue();
  }

代码很简洁,先拿到 Map,再看对应的 key 值 能不能取到 Entry,能拿到,返回 value。

其它情况返回 setInitialValue()——这个方法太简单,不值得讲,自己看吧,其最终是返回 null。

remove() 这个方法源码,涉及更深层的东西,等会再讲。记住 它是把 Map 中 key 值 设置为 null.

四、弱引用
  static class ThreadLocalMap {

      static class Entry extends WeakReference> {
          
          Object value;

          Entry(ThreadLocal k, Object v) {
              super(k);
              value = v;
          }
      }
      
      ……
 }

ThreadLocalMap 这个是定义在 ThreadLocal 类中的一个内部类。特殊之处是 key 是弱引用。

  • 从哪里看出是弱引用的?

... extends WeakReference 这里写的很清楚。

  • key 是弱引用,有啥用?

设想这么一个场景,用线程池的时候,

Thread 是有可能一直存活,那它的属性 threadLocals 会一直存在。

当这个线程在执行 A 方法时,创建了一个 ThreadLocal 实例 a,set 了一个值,当 A 方法结束后,a 对象被 GC 回收。

随后,该线程又执行了 B 方法,假设也创建了 ThreadLocal 实例 b,set 了值,B 方法结束时,b 对象被 GC 回收。

对于线程来说,不管 实例 a、实例 b 是否被 GC 回收,在 threadLocals 这个Map 里,会有其相应的两个 Entry。

情况就是 实例a 实例 b 已被GC 回收,但线程中,对应的 Entry 却依然存在,典型 占着茅坑不拉屎。

在源码的注释中,管这样的节点叫 “stale entries” ,本篇咱就叫它失效节点。

这样的节点可能会越来越多,直到 OOM。

ThreadLocalMap 中 key 设计为 弱引用,就是针对 “stale entries”,当 实例 a 、实例 b 被 GC 回收后,

其对应的 Entry 的 key 就是 null,会有专门的方法清除这样的结点,从而避免 OOM。

不过话又说回来了,内存溢出没那么容易,开篇我写了个 demo,执行了几十分钟,没有 OOM。

int 的最大值,是个很大的数字了。

好了,至此为止,ThreadLocal 讲的内容,应对面试已经没问题了。

下面开始更深入的解析源码,很枯燥,没耐心的,可以劝退啦哈!

五、魔数 0x61c88647

ThreadLocalMap 是个 Map,

初始容量是 16,之后扩容,新的容量是旧容量的 2倍。

即容量一定是 2 的 n 次方

对应的 哈希函数 是 一个数字 & (容量 - 1)

  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  
  private final int threadLocalHashCode = nextHashCode();   
   
  private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
  }
  
  private static AtomicInteger nextHashCode = new AtomicInteger();
  
  private static final int HASH_INCREMENT = 0x61c88647;

前面写过一篇文章《hash & (n - 1) 是什么》,本篇不再做解释。

ThreadLocalMap 的哈希函数,是 从 0 开始,每次加 0x61c88647 ,再和 (容量-1)做位运算。

我写了代码模拟这个过程,看到结果后,我大受震撼!!!

    public static void main(String[] args) {
        int count = 64;
        List list = new ArrayList<>(count);
        AtomicInteger nextHashCode = new AtomicInteger();
        int HASH_INCREMENT = 0x61c88647;
        for(int i = 0; i < count; i++){
            int hash = nextHashCode.getAndAdd(HASH_INCREMENT);
            int index = hash & (count -1);
            list.add(index);
        }
        System.out.println(list);
    }
-----------------打印结果如下------------------------
[0, 
7, 14, 21, 28, 35, 42, 49, 56, 63, 
6, 13, 20, 27, 34, 41, 48, 55, 62, 
5, 12, 19, 26, 33, 40, 47, 54, 61, 
4, 11, 18, 25, 32, 39, 46, 53, 60, 
3, 10, 17, 24, 31, 38, 45, 52, 59, 
2, 9, 16, 23, 30, 37, 44, 51, 58, 
1, 8, 15, 22, 29, 36, 43, 50, 57]


没有哈希冲突,0x61c88647 这个数字为什么可以做到没有哈希冲突。

我无法表达我的感受,贴张图意思一下。

HASH_INCREMENT = 0x61c88647,它对应的 十进制 数字是 1640531527,它就是魔数。

为什么每次加上它之后,算出来的哈希值没有冲突,且分布均匀?

我上网搜索了下,网上是这么说的。

2 的 32 次方,这个数字与( 1 - 黄金分割比例)的乘积,就是 魔数 1640531527。

数学上的证明,和 斐波那契数列 相关,反正我不懂!

在此不仅感叹, Doug Lea 是 ThreadLocal 的作者之一,

不仅高并发的工具类写的好,高数也应该很好,人与人的差别怎么就这么大呢?

六、set 源码精讲
  public void set(T value) {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t); // 拿到 Map
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value); // 自己看源码吧,记住初始容量是 16
  }

这段没啥可讲的,直接看 map.set(this, value);

  private void set(ThreadLocal key, Object value) {
      Entry[] tab = table;
      int len = tab.length;
      int i = key.threadLocalHashCode & (len-1);

      for (Entry e = tab[i];
           e != null;
           e = tab[i = nextIndex(i, len)]) {
          ThreadLocal k = e.get();

          if (k == key) {
              e.value = value;
              return;
          }

          if (k == null) {
              replaceStaleEntry(key, value, i);
              return;
          }
      }

      tab[i] = new Entry(key, value);
      int sz = ++size;
      if (!cleanSomeSlots(i, sz) && sz >= threshold)
          rehash();
  }

代码中的 for 循环可以大致理解为:清理无效节点(上文说的 stale entries)。这个最后再讲。
另:for 循环中里的代码,处理了key存在的情况,

扣除 for 循环,剩下的代码功能也是完整的。(entry 不存在的情况)

  private void set(ThreadLocal key, Object value) {

      Entry[] tab = table;
      int len = tab.length;
      int i = key.threadLocalHashCode & (len-1); // 算出下标
      tab[i] = new Entry(key, value); // 设置 entry
      int sz = ++size; // 记录 entry 的个数
      if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清理无效节点
          rehash(); // 清理无效节点失败且达到扩容阈值时,启动扩容
  }

先看 rehash() 方法,再细说 cleanSomeSlots(i, sz)

   private void rehash() {
       expungeStaleEntries();
       if (size >= threshold - threshold / 4)
           resize();
   }

   
   private void resize() {
       Entry[] oldTab = table;
       int oldLen = oldTab.length;
       int newLen = oldLen * 2;
       Entry[] newTab = new Entry[newLen];
       int count = 0;
       for (int j = 0; j < oldLen; ++j) {
           Entry e = oldTab[j];
           if (e != null) {
               ThreadLocal k = e.get();
               if (k == null) {
                   e.value = null; // Help the GC
               } else {
                   int h = k.threadLocalHashCode & (newLen - 1);
                   while (newTab[h] != null)
                       h = nextIndex(h, newLen);
                   newTab[h] = e;
                   count++;
               }
           }
       }
       setThreshold(newLen); // 设置新的阈值
       size = count;
       table = newTab;
   }

resize() 方法还是很容易理解的 。摘录其中一段来说明

    Entry e = oldTab[j];
    if (e != null) {
        ThreadLocal k = e.get();
        if (k == null) { // 该节点是无效节点
            e.value = null; // Help the GC
        } else {
            int h = k.threadLocalHashCode & (newLen - 1);
            while (newTab[h] != null)
                h = nextIndex(h, newLen);
            newTab[h] = e;
            count++; // 记录节点的个数
        }
    }

     private static int nextIndex(int i, int len) {
     	// 如果下一个节点是最后一个节点,返回第一个节点
         return ((i + 1 < len) ? i + 1 : 0);
     }

k == null 这个分支,就是 entry 存在,但是返回对应的key,即 ThreadLocal 实例的地址不存在了,那就回收该节点。这就是所谓的 无效节点的清除。
else 分支,就是正常节点,先通过哈希函数,算出下标。(这个在魔数那讲过了)

如果出现了哈希冲突,即该节点已被占用,那就判断下一个节点,直到找到一个空节点,迁移新节点。

哈希冲突解决办法,常见有两种

  • 链表法,比如 HashMap
  • 开放寻址法,比如今天所讲的 ThreadLocal

rehash() 方法,还剩下 expungeStaleEntries() 没讲

   private void expungeStaleEntries() {
       Entry[] tab = table;
       int len = tab.length;
       for (int j = 0; j < len; j++) { // 遍历所有节点,发现 无效节点,清除
           Entry e = tab[j];
           if (e != null && e.get() == null) // entry 存在,但 key 不存在
               expungeStaleEntry(j);
       }
   }

 private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // expunge entry at staleSlot
        tab[staleSlot].value = null; // 清空 vaule
        tab[staleSlot] = null; // 清空 该节点
        size--;

        // Rehash until we encounter null
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal k = e.get();
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;

                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

expungeStaleEntry(j) 这个方法,是在判断该位置是 无效节点时调用的。

清除该节点之后,工作已经做完了。它又来了个 for 循环。还是清除节点。

我画了张图,来说明问题,比如调用 expungeStaleEntry(j) 时,j = 1。

那 for 循环那段代码,就不进去,因为 下标 为 2 的那个节点是空节点。

假如调用 expungeStaleEntry(j) 时,j = 4,

那 for 循环将处理 5 至 8 四个结点(遇到空节点退出for循环)

5 6 7 这三个节点,会进第一个分支 if (k == null) ,清除无效节点。

节点 8 进入 else 分支。此时 i = 8

    int h = k.threadLocalHashCode & (len - 1); // 重新计算下标
    if (h != i) {  // 说明需要移动位置
        tab[i] = null;
        while (tab[h] != null)
            h = nextIndex(h, len);
        tab[h] = e;
    }

假如 重新计算下标,得到 h = 4,此时,先将下标为8的节点设置为空。

再从下标为 4 开始向后遍历,直到找到空节点,将 该节点指向 entry。


至此 rehash 方法讲完了。

  • cleanSomeSlots(i, sz)
    回过头再看下,清除节点失败,且达到阈值,才调用 rehash() 方法启动扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();

cleanSomeSlots 这个方法有点复杂,我慢慢说

   private boolean cleanSomeSlots(int i, int n) {
       boolean removed = false; // 默认没有无效节点
       Entry[] tab = table;
       int len = tab.length;
       do {
           i = nextIndex(i, len);
           Entry e = tab[i];
           if (e != null && e.get() == null) {
               n = len;
               removed = true;
               i = expungeStaleEntry(i);
           }
       } while ( (n >>>= 1) != 0);
       return removed;
   }

调用 cleanSomeSlots(i, sz) 时,i 这个下标,设置一个新的 entry,sz 指数组中 entry 的总个数。

    do {
		……
        }
    } while ( (n >>>= 1) != 0);

这个循环,时间复杂度,一定不是 O(n),应该是 O(log n)。

如果 n = 2 的 10 次方,那 n >>>= 1 得到就是 2 的 9 次方。

即 这个循环,会执行 10 次,然后再看循环里的代码

   i = nextIndex(i, len);
   Entry e = tab[i];
   if (e != null && e.get() == null) {
       n = len; // 重置 n 
       removed = true; // 标记下扫描到了失效节点
       i = expungeStaleEntry(i); // 上文讲过这个方法了。
   }
   

这里当扫描到 失效节点时,会重置 n,

即扫描到失效节点,就扩大扫描范围,这个不细讲了,自己体会吧。

expungeStaleEntry 这个方法,上面详细讲过了,没有印象的话,翻回去再看看。

至此 set 方法讲了一半,(entry 不存在的情况)

总结下逻辑: entry 不存在时,

  1. 创建新节点。
  2. 扫描下一个节点,如果是失效节点,清除,并扩大扫描范围。
  3. 符合条件时,调用 rehash 方法,刷新整个数组,清除失效节点。
  4. 必要时启动扩容,数组容量翻倍。

下面讲另一半的源码,entry 存在时的逻辑

先明确一点,这种情况下,一定有空节点的存在。

为什么?你往上翻翻 rehash 方法,没有空节点时,一定会触发扩容。

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

if (k == key) 这个分支的代码,很好理解,直接设值,结束。

if (k == null) 这个分支,指这是一个失效节点,调用 replaceStaleEntry,

此方法第三个参数,失效节点 的下标。

  private void replaceStaleEntry(ThreadLocal key, Object value,
                                 int staleSlot) {
      Entry[] tab = table;
      int len = tab.length;
      Entry e;
      int slotToExpunge = staleSlot; // slotToExpunge需要清除的节点下标 
      for (int i = prevIndex(staleSlot, len);
           (e = tab[i]) != null;
           i = prevIndex(i, len))
          if (e.get() == null)
              slotToExpunge = i;

      for (int i = nextIndex(staleSlot, len);
           (e = tab[i]) != null;
           i = nextIndex(i, len)) {
          ThreadLocal k = e.get();
          if (k == key) {
              e.value = value;
              tab[i] = tab[staleSlot];
              tab[staleSlot] = e;
              if (slotToExpunge == staleSlot)
                  slotToExpunge = i;
              cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
              return;
          }
          if (k == null && slotToExpunge == staleSlot)
              slotToExpunge = i;
      }
      tab[staleSlot].value = null;
      tab[staleSlot] = new Entry(key, value);
      if (slotToExpunge != staleSlot)
          cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  }

这是一个能把人看哭的代码,不好理解,我慢慢说。

先记住这个方法第三个参数 staleSlot,是要失效的节点的下标。

slotToExpunge 是要清除的节点下标,初始值是 staleSlot。

	int slotToExpunge = staleSlot; // slotToExpunge需要清除的节点下标 
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null) // 是失效节点
            slotToExpunge = i;

这个是后退循环,遇到空节点时退出。(一定会存在空节点,前面说过了),

在此期间,遇到失效节点,就重置 slotToExpunge

借用前面的那张图,假如 调用 replaceStaleEntry 方法时,staleSlot 是 5

扫描下标为 4 的结点,不是失效节点,进入下个循环。

下标为 3 的结点是空节点,跳出循环,此时slotToExpunge 还是5

   for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal k = e.get();
        if (k == key) {
			……
            return;
        }
        if (k == null && slotToExpunge == staleSlot) // 遇到失效节点,且后退循环没找到失效节点
            slotToExpunge = i;
    }

这是个正循环,扫描到 空节点退出循环。结合上图,该循环,可能扫描 6 7 8 这三个节点,

扫描到下标为 9 的结点,退出循环。

在此期间,

  • 如果遇到同样的 key ,执行一些 *** 作,结束。
  • 如果遇到失效结点,且后退循环没有遇到失效节点,重置 slotToExpunge,直到跳出循环后,执行某些 *** 作,结束。

先讲 遇到同样的key,这种情况

     if (k == key) {
         e.value = value; // 设置值。
		// e 与  tab[staleSlot] 互换位置
         tab[i] = tab[staleSlot];
         tab[staleSlot] = e;

         // Start expunge at preceding stale entry if it exists
         if (slotToExpunge == staleSlot)
             slotToExpunge = i;
         cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
         return;
     }

还以上面的图来说,假设 i = 8 时的那个循环,进入了这个分支。

下标为5的节点,与下标为8 的结点,互换位置。

即下标为8的节点,是失效节点,下标为5的位置,是 存着 key 与 vaule。

if (slotToExpunge == staleSlot) 成立,slotToExpunge 的值变为8。

(不成立时,自己想想吧,不明白可以留言哈)

执行 expungeStaleEntry(slotToExpunge),即清除下标为8的结点,这方法上面讲过了。

执行 cleanSomeSlots,即扫描后继结点,必要情况下扩大扫描范围。上面也讲过了。

至此,遇到同样的key的情况讲完了,现在讲另外的逻辑——没有遇到同样的key

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

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

前两行很容易理解,没有遇到同样的key,那直接在 staleSlot 这个下标的位置,

将旧的 value 清除,设置新的 key 和 value。

if (slotToExpunge != staleSlot) 这句的意思是,后退的那个 for 循环,扫描到了失效节点,

那执行expungeStaleEntry 清除该节点, 执行 cleanSomeSlots 进行后继节点的扫描。

总结下逻辑: entry 存在时

  1. 遇到同样的key,覆盖旧值,结束。
  2. 遇到的是失效节点,在后继节点,找到同样的key,交换,清除无效节点。
  3. 后继节点没找到同样的key,直接将失效节点转换掉。

顺便说一句: entry 存在时,代码很复杂,是因为 哈希冲突时,采用线性探测的方法来解决。

至此 set 方法讲完了,分为两种情况,entry 存在,与 entry 不存在两种情况。

代码复杂是因为两个方法:线性探测 和 清除无效点的处理。

七、get 方法略讲
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); // 拿到 Map
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue(); // 这个源码不用看,反正是返回null
    }

这段代码很清晰,只讲 getEntry 方法

    private Entry getEntry(ThreadLocal key) {
        int i = key.threadLocalHashCode & (table.length - 1); // 计算下标
        Entry e = table[i];
        if (e != null && e.get() == key) // entry 存在且 key 相同
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
    
     private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
         Entry[] tab = table;
         int len = tab.length;

         while (e != null) {
             ThreadLocal k = e.get();
             if (k == key)
                 return e;
             if (k == null)
                 expungeStaleEntry(i);
             else
                 i = nextIndex(i, len);
             e = tab[i];
         }
         return null;
     }

getEntryAfterMiss 这个方法中,while 循环,是一直往后找,

直到找到相同的key,或者遇到 空节点退出。

期间如果遇到失效节点,执行 expungeStaleEntry 清除失效节点。

总体代码不难,也是解决 线性寻址 的问题,和清理无效节点的问题。

八、remove 方法略讲
  public void remove() {
       ThreadLocalMap m = getMap(Thread.currentThread());
       if (m != null)
           m.remove(this);
   }
   private void remove(ThreadLocal key) {
       Entry[] tab = table;
       int len = tab.length;
       int i = key.threadLocalHashCode & (len-1);
       for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
           if (e.get() == key) {
               e.clear();
               expungeStaleEntry(i);
               return;
           }
       }
   }

    public void clear() {
        this.referent = null;
    }

调用 remove 方法,逻辑就是找到对应的 entry,

清除 key,即entry 变为失效节点,再清理失效节点。

若 key 相等,

九、小结

有没有发现,ThreadLocal 中,没有 CAS *** 作,也没有用锁,不会产生并发问题么?

不会!!!

map 是线程本身的属性,和线程名一样,每个线程都有,各自用各自的,不用抢。

set、 get、 remove 这三个方法,都会清理失效节点,

所以用 ThreadLocal时,大可不必担心,内存溢出的问题。正常使用,没问题的。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5680918.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存