ThreadLocal 源码 JDK8

ThreadLocal的学习一开始是比较绕的,慢慢的看源码思考,后来就能习惯了。

对于学习这个类的使用,我们必须搞清楚三者之间的关系:

ThreadLocal

ThreadLocalMap

Thread

ThreadLocalMap是作为一个内部静态类嵌套在ThreadLocal中的,而当它具象化时(也就是new了)它作为对象存在后,又会被Thread持有引用(地址或指针)。

ThreadLocalMap类是一个基础的类似HashMap的类啦。但是功能远没有那么强大。

是一个 Entry[] 数组,提供了 set() get() 函数。初始化容量 16,hash过程是利用 ThreadLocal 的nextHashCode & table.length - 1

算出 key 的具体放置位置,然后遍历判断(一些规则),找出空位或相同key,插入。

ThreadLocalMap set() 函数。比HashMap 简单太多..  Hash冲突解决只是简单的线性探测。

每次set完后,符合条件会扩容,因此没有链表更没有红黑树...这些复杂的冲突解决办法。

get() 函数也很简单

其中 nextIndex() 就是线性探测。

这里说下为什么要遍历 table 插槽找 entry。因为一个线程往 ThreadLocalMap 容器存值的时候,总是以 ThreadLocal 为 key。

而相同的 ThreadLocal 就会存在相同的 solt 中,会覆盖旧值。但不同的 ThreadLocal 有可能有相同的 hashcode,而 ThreadLocalMap 是以线性探测解决哈希冲突。所以一开始计算的 hashcode 所处的 solt 位置的 entry 不一定是正确的,可能因为哈希冲突被放到后面去了,所以遍历 table 一直找到最尾部。

resize() 函数(rehash()函数实际调用的扩容函数)

也很简单,简单地将数组扩大2倍,一个新的Entry[]数组。遍历将 entry 按照hash算法放置在新数组的新位置。

最后来看下 Entry 类。作为真正存储数据的类型,它是对数据没有泛型要求的。

即并没有限定这个ThreadLocalMap存储某一种类型。

Entry 声明WeakReference类型是ThreadLocal,也就是Entry中key是弱引用,指向某个ThreadLocal。

那为什么 Entry的key为什么要被声明为弱引用?这里涉及内存泄漏话题,具体可看Follow Up

在 expungeStaleEntry() 中对当前线程的map中存在的 null key 做清理,这是ThreadLocal预防内存泄漏的机制,只要当前线程的local map 有在被使用就可能可以遍历到null key,null key 所在的entry就被清理。

学习完ThreadLocalMap,就可以看 ThreadLocal 了,它提供了API操作,set() get()

set() 函数可以看到 拿到当前线程的 map,不是空就将 this 即当前某个 ThreadLocal 作为Key绑定 value 放到

这个线程的map中去。否则就创建map咯。

可以看到 map 没有泛型限制,意味着可以放任何类型 threadlocal

再看 get(),同样拿到当前线程 map ,再以当前调用的 threadlocal 为 key 拿到当初

这个线程在这个 threadlocal 上存进去的数据。其中 map.getEntry() 前面讲过了。

如果线程的map没创建的话,会被创建

对于使用 ThreadLocal 存储数据时,可以看做是向线程的map中存数据,只不过是将这个 threadlocal 对象作为key和我们的value绑在一起,

标记一下,我们用这个 threadlocal 存了个数据而已。通过上面的源码可知,同一个线程,对于同一个 threadlocal 实际上只能绑定一个value

存到自己的map中去,因为第二次绑value时,会覆盖前一个value。所以通常的使用是线程配合多个 threadlocal 来绑定数据存进map中。

下面的测试中,对于 main 线程,绑了2个数据进去,输出结果 1,2

总结:

可以将 ThreadLocalMap 看为容器,Entry 看为房间,ThreadLocal 看为能打开对应 Entry 的钥匙,每个Thread都有属于自己的容器。

所以每当线程存取数据时,就相当于拿着不同的钥匙(ThrreadLocal)去自己的容器中(ThreadLocalMap)找到这把钥匙对应的房间(Entry),

拿到房间中的存储数据。

Follow Up:ThreadLocal使用不当导致的内存泄漏

在内存中的分布图。

在看了一些网络资料后觉得ThreadLocal没有那么脆弱,不会很轻易的就内存泄漏,它只会在特定的场景出现。

所谓的内存泄漏就是某个内存变量长期存在,没有办法找到它、使用它、清除它。

这里可以回答为什么 Entry 中把key(ThreadLocal的引用)声明为弱引用?

可看到 ThreadLocal 有2个引用,栈中的强引用和Entry中的弱引用。试想Key是强引用,某个强引用ThreadLocal的变量被GC回收了,在Stack中的强引用就不存在了,但是key还是强引用,这个ThreadLocal还是不会被回收呀,但是你又没办法使用到它,因为代码中使用它的引用丢了...

那么以这个ThreadLocal为key的相关线程中的LocalMap中一些entry就被内存泄漏了。而key是弱引用,在下次GC时就把ThreadLocal给回收,OK。

还有个问题,既然ThreadLocal被回收了,那么用它作为key存数据的线程,它们的map中对应Entry就没办法被访问到。那Entry不就泄漏了吗?

所以这里可以回答为什么 expungeStaleEntry() 存在的原因,就是去清除 (null key -> value) 这种内存泄漏的Entry的。

说了这么多,所以ThreadLocal有了key 弱引用和主动清除 null key的机制会不会有内存泄漏情况发生?

一般情况下不会,但是在使用不当有可能会,例如:某个线程生命周期很长,它使用了某个ThreadLocal a作为key存了数据在它的map中,然后它存进去后在使用完数据后又不清除它,但是这个数据在以后的业务中都不会被用到了,也就是这个a不会被用到了。

那么a可能作为类变量长时间存在,这个线程又很长命,相当于map随着时间的推移一直被存在内存中,这种现象是另一种内存泄漏吧。

另外用完数据后不清除,在复用线程时会拿到之前的旧数据,就可能导致业务逻辑错误!这是更可怕的地方。

所以 ThreadLocal 和ThreadPool结合一起使用时,必须每次用完localmap 就得remove()数据,避免下一个Job用到旧数据。

参考文章:

ThreadLocal原理及内存泄露预防_puppylpg的博客-CSDN博客_threadlocal内存泄漏ThreadLocal原理及内存泄露预防

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值