ThreadLocal总结一:问题及其理解汇总

背景
  1. 整理ThreadLocal重要问题及其相关理解。
过程
  • 如何分析内存泄漏

    1. 首先,ThreadLocal作者认为如果ThreadLocal实例不在被线程使用了的话,那么ThreadLocal实例就没有存在的必要了。一旦线程不使用了,那么下次GC的时候就会把ThreadLocal移除掉。因此把ThreadLocal设计成弱引用。
    2. 其次,在当前线程执行的一段时间内,如果大量积累值 v , 那么就会出现内存泄漏。因此,内存泄漏研究的是哪个?是值 v 。
    3. 然后,理解GC的逻辑是,不管内存是否充足,如果ThreadLocal不在被线程使用了,那么下次GC的时候,就会被清除掉。
    4. 最后,需要深刻理解,如果线程退出了或者说线程执行完成了,这些相关实例会被回收的,不会导致内存泄漏。而事实却不是这样的,因为线程执行有一个时间段的,就是在一个连续的时间段内,我们对ThreadLocal进行了计算和处理,但是线程后续逻辑却不需要再使用ThreadLocal实例了,那么这个时候,我们有必要把容器中entry[]的key和value都置null,这样对堆上的对象没有了使用,下次GC的时候就会回收。
    5. 注意:其实,ThreadLocal设计成弱引用,这也是一种保护机制,因为至少可以减少内存空间的占用,如果把减少内存空间的占用理解为被及时GC掉了,那么也可以理解为,这样的设计也是防止内存泄漏。只是作用不是那么明显,因为ThreadLocal在堆上仅仅只有一份。但是,v也是只有一份吗?不是的,每一次更新,可是新创建的对象,因此v又很多对象。虽然是很多对象,但是有用了,仅仅只有一个,其他都会被GC掉。那也只有一份,为什么会内存泄漏呢?是因为在线程执行的时间段内,有大量的线程,大量的线程就有大量的v,而大量的v并不像弱引用的ThreadLocal会被及时GC掉的。
  • 为什么容器被设计成Entry[]?

    1. 首先,考虑实际情况对ThreadLocal的使用,很少遇见使用了多个ThreadLocal实例的情况,如果一定需要使用多个ThreaddLocal实例的时候,我们完全可以定义一个引用对象,让这个对象持有相应的变量即可。
    2. 然后,设计成Entry[],从表面上看,那么就是能够装不同的ThreadLocal实例。
    3. 最后,猜测,作者对这个ThreadLocal有更加深层次的考虑。希望这样的设计能够满足各种各样使用场景。
  • 同一个线程中使用多个ThreadLocal实例发生了什么

    1. 首先,虽然每个ThreadLocal实例都是new出来的,他们都是彼此独立不一样的。但是,他们却共用了一个原子类,这个原子类记录了,当前线程,取到原子类中数值是多少。
    2. 那他们是怎么做到呢?就是当第一次new的时候,会加载类,连接,初始化。其中,初始化的时候,就给ThreadLocal实例的属性threadLocalHashCode设置值并返回一个值。同一个线程,执行第二次new的时候,不需要加载类了,它会执行初始化操作,这个时候取得上次设置好了的值,然后自己在设置一个新值,供下一个new ThreadLocal来取值即可。
  • ThreadLocal为什么设计成弱引用?

    1. 原因很简单,那就是,如果当前线程不再使用了ThreadLocal实例了,那么这个ThreadLocal实例,就没有存在的必要的了,无论内存是否充足,都希望下次GC的时候,能够清除掉它。
    2. 设计成弱引用好处就是,能够及时被GC掉,这样可以释放资源。
  • ThreadLocal是如何解决hash冲突的

    1. 通过线性探测方式,线性地找到下一个脚标中key为null的情况。以此解决hash碰撞。为什么叫线性探测法呢?线性是说,下脚标依次取值,为什么叫探测呢?因为它也不知道下一个脚标是否有值还是没有值,有还是没有值,尝试一下就知道了。因此叫做:线性探测法。
    2. 又因为这个ThreadLocal的属性threadLocalHashCode & (length - 1)这个值求出来是不规则的,因此,需要循环检测。这里的循环检测,就体现了这个环形数组。因为循环到结尾的时候,又会回到最开始的地方。
  • ThreadLocal为什么要设计环形数组?

    1. 因为环形数组可以节约内存空间,重复利用已经分配好的空间。这里的本质原因是希望减少扩容的次数。因为每次扩容,都需要分配原来的2倍空间,把旧的entry[]中的内容完全拷贝到新的entry[],需要一些计算过程,会耗性能。如果能够减少扩容的频次,那么性能就会很高。这里当然是考虑平均性能,因为发生扩容的概念是小的。

    2. 如果没有环形数组,扩容的频次一定会增加的,而且内存占用会更多。

    3. 如果使用了环形数组,不但节约了内存空间,还减少了扩容的次数。

  • 环形数组会增大hash碰撞的概率吗?
    是。

    假如我们的GC都是非常及时的,而我们的魔数相关算法又是循环的。所以一定会产生碰撞的,而我们的存储空间又是环状的,魔数和环形数组一起使用确实会增大hash碰撞的概率的。这是一种推测,在科学领域,只要存在一个可能的反面,那么这样的推测就是合理的。

    当然,也有可能我们GC不及时,那么一些无用的entry无法被及时清除掉,而又占用了table的空间,这个时候,就可能会发生扩容,而扩容一旦发生,那么发生的hash碰撞的概率就会减少。也就是length越长发生的概率就会越低。
    作者想节约内存空间和减少扩容频次,于是设计了环形数组。这个环形数组带来好处,却加大了hash碰撞的概率。大师之所以是大师,是因为他有权衡的智慧。他认为节约空间并且减少扩容频次带来的性能提升和资源利用的好处是大于增加hash碰撞概率缺点的。因为,无论怎样,作者的代码设计一定是需要解决hash碰撞的。也就是说,作者无论是否使用环形数组,都需要写解决hash碰撞的逻辑。

  • 用魔数0x61c88647来解决hash碰撞,可行吗?

    使用算法threadLocalHashCode & (length - 1)。完全可行的。因为从做的测试来说,是均匀完美散列的。完美散列是指16长度,然后产生0-15中的每个数字。

    确实可以满足解决hash碰撞。但是,作者把ThreadLocal设计成了弱引用。在实际使用过程中,可能存在有的ThreadLocal已经完成了职能,被GC掉了,这个时候,entry[]中的某个具体的entry就不在占用空间了。而我们知道魔数的特性是循环的,那么一定会产生hash碰撞的。

    因此,作者这样的实现是一定会存在hash碰撞的。

  • threadLocalHashCode & (length - 1)求得的值是不规则的?

    1. 首先,求得的值是完美散列且是均匀的。
    2. 其次,后面在执行set操作的时候,会执行cleanSomeSlots()函数,这个函数是试探性地去除无效的entry实例,并不能保证一定能够把无效的entry移除掉。它循环判断的次数是以2为低,长度为指数的log函数。因此,方法名some,表达了可能是0个,可能是1个,也可能是多个。
    3. 最后,因为这样的散列是均匀而完美的。后续的log函数次数的遍历才更加合理。但是,有的会认为,为什么不直接遍历数组呢?没有必要,因为遍历整个数组,会耗费性能的。这样每次执行set操作的时候,都需要遍历一次数组。因为能够及时被GC掉的ThreadLocal,也不是频繁发生的。完全没有必要遍历一次数组。因为,遍历一次不一定能够找到,为什么不少执行几次,试探性删除呢?因为这里的删除并不是必须的。
小结
  1. 如果能够静下心来,把基本知识、工作过程、源码细节通读后,一定会理解背后为什么这样设计。
  2. 在这个过程中,需要提出很多问题、需要去找资料、需要写测试代码、需要分析、需要读源码、需要去想、需要总结才可以最终理解。
  3. 还有很多其他问题,ThreadLocal在不同线程间访问呢?父线程访问子线程的值呢?
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值