ThreadLocal-家常唠嗑版

   我们公司在过代码的时候,有一位同事在用ThreadLocal 的时候被另一位同事说,要remove一下。我当时想threadLocal 不是自己有解决内存泄漏的机制吗,就没有太注意。事后 看了一下Threadlocal 的源代码,感觉也比较简单 也没上心。但是后来开发中越想越不对劲,感觉 越往里深想,好像很多问题。他是如何实现线程间不影响的?,反正我第一眼看到Threadlocal 的时候我肯定 想着用线程id当作key,然后值当作value。 因为一个Threadlocal 就一个值 对不对。其次是他如何 防止内存泄漏的 。等等这些问题吧,接下来我们一起看一下。

1.首先说一下 为了配合Threadlocal jdk 为每一个Thread 里边维护类一个变量ThreadlocalMap 这个ThreadlocalMap 是一个Threadlocal 的内部类,每一个线程都有自己的ThreadlocalMap 这也是线程之间的变量不相互影响 核心逻辑, 但是ThreadlocalMap 他不是真正意义上的map,他只是一个数组 ,当遇到哈希冲突的时候 用的开放寻址法解决,而不是像 map那样 通过红黑树解决 哈希冲突。

知识扩展,开放寻址法, 当一个数组中,遇到哈希冲突的时候,他会在下一个 空位置存进去。(具体怎么找这个空位置 有很多算法,反正目前就是为了 效率更高),举个例子,本来计算的哈希值应该存在数组中3号位置,但是3号位置 已经被占了。那么咋办 就看看4号 是不是空的,4号也被占了就一次继续找5号 直到找到 空位放进去。

2. Threadlocal 为啥不用线程id 作为key 呢 ,当时我也这样想的,因为一个线程Thread id 是唯一的 ,但是一个线程从开始到结束,中间会经历不止一个Threadlocal ,比如一个线程过来,代码把用户信息存到Threadlocal 1,把合同信息存入到Threadlocal2, 把贷款明细存到Threadlocal3 ,那么你的线程ID 是唯一的, 当你Threadlocal.get() 的时候取得是啥 。

所以Threadlocal 的设计是以Threadlocal 本身(this)作为key。这样哪个Threadlocal.get() 系统就知道去取哪个,比如Threadlocal2.get()那么肯定取得 合同信息。

3.threadlocal 是如何解决内存泄漏的,你想想在使用线程的话肯定 用线程池吧,你不可能直接new 一个线程吧,线程池中的线程都是一直存在的,那么Thread 里边 的ThreadlocalMap 中的数据会越来越多咋办。他的解决办法是 thread本身被套一层弱引用,一但设置成弱引用,那么会有被jvm 回收的可能。

我们在看源码部分

1. set方法

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

        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
  // 每一个Threadlocal 都有一个唯一的threadLocalHashCode用于计算槽位 //注意这个是固定的 threadLocalHashCode 不会改动 计算出槽位为i
        int i = key.threadLocalHashCode & (len-1);
        
        for (ThreadLocal.ThreadLocalMap.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 ThreadLocal.ThreadLocalMap.Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
}

这里我们看到循环部分 ,当if (k == key)  好说,说明找到了 直接返回value 就可以 ,这里有一个细节 ,还记得开放寻址法吗, 也就是说 即使计算出这个槽位是i 也不一定是 真正你要找的,因为遇到哈希冲突的时候 ThreadLocal 要依次往后找直到找到if (k == key)时候返回 value。

但是 在找的过程中,需要干点活,干啥活呢当if (k == null)  的时候说明被回收了,你想 entry 有值但是 entry 中的key 没有值。replaceStaleEntry 翻译过来就是 替换掉过期的Entry。

当这个循环结束,还是没有找到咋办呢,那说明这是一个新的 ,走新增逻辑这个好理解。

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

int sz = ++size;(个数加一)

接下来说一下这个重点:replaceStaleEntry()

看一下源代码

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    ThreadLocal.ThreadLocalMap.Entry e;
    int slotToExpunge = staleSlot;

    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 ThreadLocal.ThreadLocalMap.Entry(key, value);
    if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

咱们在想一下,为什么进入这个方法replaceStaleEntry, 如果计算出来的i ,对应的entry 有值但是key为null 说明啥,这个位置是哈希冲突的位置,依据开放寻址法我在这个位置为起点开始依次往后找,当我找到了 ,那没事我把对应的value 修改了就可以,但是你别忘了。为了提高效率我要把这个位置提到i这个地方来,为啥呢,因为i这个地方已经key为null 了,没人用了 我不如先到先的 把这个位置占了,等下次 get 的时候 计算出来i 直接就能拿到,不用在 i 之后 继续往后找了。对不对 如果还想不通 你就这样想,你本来你家楼下停车,但是 你家楼下车位都满了 ,你不得不停到离你家楼下稍微远一点的地方,但是有一天你发现在停车时,突然占你楼下的车位 走了 ,你肯定赶紧占上 你不可能 还跑离你家比较远的地方停吧。

代码继续 ,我找了一圈 都没有找到那咋办,很简单我就直接用这个位置了 ,后边的整个代码我都不用走了 这也是为啥 if (k == null) 的时候直接return;

在看方法里边:首选看一下第一个循环,在staleSlot 位置 一直往前找, 举个例子 有一个10长度的数组,从staleSlot 开始往前找,如果找到0,就 直接跳到数组的末尾继续往前找 ,直到找到 一个真正的空位才停下(即=null),在这个循环中如果发现有其他的过期槽位就把它给slotToExpunge。

接下来看第二个循环,从staleSlot往后找, 如果 发现了if(k==key) 说明找到了对应的key,那好办 直接替换掉value, 然后就是 刚才我说的交换,

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

你要站离你家最近的位置,你不可能在跑之前离你家远的那个了对吧。

代码继续,基本上完成了所有功能,但是后续工作

cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

其实 就是把 所有那些过期的entrty 进行清理, 这个自己看一下吧 。但是不难理解。

第二个循环结束,说明没有找到指定的key那么 就需要重新 新增一个 entrty 放到当前的 

staleSlot 位置上 
tab[staleSlot].value = null;
tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

关于 staleSlot 和slotToExpunge 相等和不相等,这个我简单说一下, 相等的时候 表示从数组找了一圈发现没有找到 最终找到了 staleSlot 这个位置 ,所以staleSlot 和slotToExpunge 相等了 因为(staleSlot 在replaceStaleEntry方法里边肯定会entrty 的key =null),不相等好理解,说明在staleSlot之前 可肯定也有过期的,那么 要从staleSlot之前位置开始清理 即slotToExpunge

 expungeStaleEntry这个方法也说一下,消除过期的Entrty, 这个方法整体上就是 把过去的entrty 制为null,但是源码里边有一段代码片段是这样的

if (k == null) {
    e.value = null;
    tab[i] = null;
    size--;
} else {
    int h = k.threadLocalHashCode & (len - 1);
    if (h != i) {
        tab[i] = null;
        while (tab[h] != null) h = nextIndex(h, len);
        tab[h] = e;
    }
}

if 不等于null 等时候, 你想想 说明这个槽位已经有正在使用的数据,为啥还需要处理呢,因为也是为了提高效率尽可能的最早找到数据, 你看下 计算一下槽位h 如果h等于i好说 不用管,get 的时候计算出来直接拿到 因为没有冲突,但是 计算出来后发现 h和i不相等,那么就要吧这个数据放到最近的一个空槽位了。

ThreadLocal 的get 方法 不分析, 和set 方法 很多相似 可以自行分析。

 

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值