ThreadLocal从变量副本的角度解决多线程并发安全问题

26 篇文章 6 订阅
8 篇文章 0 订阅

ThreadLocal从变量副本的角度解决多线程并发安全问题

之前我们讲的高并发场景下的线程安全问题,可以使用Synchronized同步关键字、Lock手动加锁的方式去解决,什么轻量级锁、偏向锁、重量级锁、可重入锁等等,实际上本质都是控制线程,使得多个线程同步的去访问共享资源。之所以多线程存在线程安全问题,就是因为多个线程访问同一个共享资源导致的,多个线程之间属于竞争关系,很容易就会导致数据的不安全。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nh9HtNqq-1633258255949)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003044313949.png)]

我们说了加锁实际上保证了各个线程之间同步、有序的去访问共享资源,难道不加锁就没有办法解决多线程安全问题了吗?我们要抓主要矛盾,之所以存在并发安全问题,是因为共享资源只有一个,多线程会竞争获取共享资源,如果同一类共享资源有多个,或者说有多个副本给每一个线程使用呢?这样不就不用加锁了,每一个线程只需要存在自己的那个数据的副本即可,因此也就不存在资源竞争问题,也就保证了多线程下数据的安全。而ThreadLocal类就是给每个线程绑定了变量的本地副本,从而避免线程安全。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EKDJz1YS-1633258255952)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003044351993.png)]

接下来我们从源码来看看ThreadLocal类是如何给每一个线程保存变量的本地副本的。

首先我们看看Thread线程类,线程类里面有一个ThreadLocalMap类型的成员变量threadLocals,用来存放当前线程的本地变量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9paSKNhs-1633258255955)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003044849554.png)]

实际上ThreadLocal类有一个ThreadLocalMap内部类,这个内部类你可以认为是一个专门用来维护线程本地变量HashMap集合

ThreadLocalMap这个类的数据结构是一个Entry类型的数组,用来保存一个个的Entry节点,Entry节点封装了以ThreadLocal实例对象为keyObject对象为value的键值对,保存在ThreadLocalMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hXN7z5v5-1633258255957)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003041216097.png)]

ThreadLocal类实际上是对ThreadLocalMap这个内部类的封装本地变量值最终是存放在ThreadLocalMap中的ThreadLocal类提供了set()get()等其他方法,来操作ThreadLocalMap中保存的数据。

ThreadLocal调用set()方法保存本地变量时,首先获取到当前线程,然后调用getMap()方法得到当前线程的ThreadLocalMap,底层实际上是调用ThreadLocalMapset()方法向这个Map集合中保存数据的在这里插入图片描述

当第一次调用set()方法时,会先调用createMap()方法创建出ThreadLocalMap对象,因此是懒加载的
在这里插入图片描述

接着会使用构造方法创建出ThreadLocalMap对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kiX42MxN-1633258255966)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003045634766.png)]

我们之前说过ThreadLocalMap是一个HashMap集合,因此也有初始容量、加载因子、阈值、散列函数、hashcode值。

ThreadLocalMap的初始容量默认为16,阈值为容量的2/3,利用ThreadLocalhashcode值,对容量进行取模运算,计算出Entry数组中对应的索引位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5EVlC2k2-1633258255968)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003050738848.png)]
在这里插入图片描述

如果ThreadLocalMap之前已经创建出来了,就会调用set()方法向ThreadLocalMap中添加元素。根据hashcode值计算出数组中对应的索引位置,然后遍历这个map所有的Entry,如果key存在了就进行替换,没有的话就将键值对保存到ThreadLocalMap中。同时在遍历的过程中发现key为null,就会清除掉这个数据,并将新的数据存放到这个索引位置。这主要是释放了内存空间,防止内存泄漏

如果既没有发生替换,也没有发生可以清除掉的key,那么就会创建一个Entry,保存到计算出的对应的索引位置。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BcCDm9C9-1633258255973)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003174955938.png)]

我们说过ThreadLocalMap初始容量默认为16,阈值默认为容量的2/3。在向map中添加完数据时,最后还会进行一次清理工作,如果清理后发现当前map的大小还是大于等于阈值,就会触发扩容机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CH22bhiQ-1633258255974)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003173537503.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9oRcu5y4-1633258255976)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003175513153.png)]

ThreadLocalMap扩容机制和HashMap差不多,也是扩容为原来的2倍,然后进行扩容后在再散列,并设置新的阈值。
在这里插入图片描述

讲完了set()方法再来讲一讲get()方法。

get()方法首先获取到当前线程,然后调用getMap()方法得到当前线程对应的ThreadLocalMap。如果这个map不为null,就根据key得到对应Entry,并返回对应的value值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TnwBbfVj-1633258255979)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20211003180603028.png)]

如果map为null,比如在第一次调用get()方法,这个map可能还没创建出来。此时会调用setInitialValue()方法来设置初始值,

并返回这个value,value的默认初始值为null。
在这里插入图片描述

我们讲完了ThreadLocal类的set()get()方法,实际上都是一直在操作ThreadLocalMap这个map集合。而ThreadLocalMap中存放的都是一个个的Entry,Entry的key为ThreadLocal对象,value为对应的本地变量。实际上这个EntryWeakReference弱引用的子类,这是为了在JVM进行垃圾回收时,能够自动进行回收,防止内存溢出。真正存储数据备份其实就是这个Entry
在这里插入图片描述

ThreadLocal类本质实际上是以线程作为key,通过数据备份的方式,实现了线程间的数据隔离!

既然进行的数据备份,那么肯定就会造成数据冗余,并且随着线程的存活时间增长,存储的备份数据也会越来越多,即使线程结束了生命周期,这些备份数据也很有可能仍然存在。这样就可能造成内存泄漏,进而导致OOM!

ThreadLocal为了解决内存泄漏的问题,也进行了一些相应的处理,比如将存储备份数据的Entry类设置为弱引用类型,这是为了方便在GC时自动回收。而且在set()get()方法中增加了数据检查,及时清除掉那些key为null的没用的备份数据。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值