ThreadLocal 详解

用处

         跨方法进行参数传递,比如 Web 容器中, 每个完整的请求周期会由一个线程来处理。 结合 ThreadLocal 再使用 Spring 里的 IOC 和 AOP,就可以很好的解决事务的问题。只要将一个数据库连接 放入 ThreadLocal 中,当前线程执行时只要有使用数据库连接的地方就从ThreadLocal 获得就行了。再比如,在微服务领域, 链路跟踪中的 traceId 传递也是利用了 ThreadLocal。

使用

void set(Object value)  设置当前线程的线程局部变量的值。
public Object get()  该方法返回当前线程所对应的线程局部变量。
public void remove()  将当前线程局部变量的值删除, 目的是为了减少内存的占用, 该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动 被垃圾回收, 所以显式调用该方法清除线程的局部变量并不是必须的操作, 但它 可以加快内存回收的速度。
protected Object initi alValue() 返回该线程局部变量的初始值,该方法是一个 protected 的方法, 显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法, 在线程第 1 次调用 get() set(Object) 时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一 个 null。

实现分析

       是让变量副本跟随着线程本身,而不是将变量副本放在一个地方保存,这样就可以在存取时避开线程之间的竞争。同时,因为每个线程所拥有的变量的副本数是不定的,有些线程可能有一个, 有些线程可能有 2个甚至更多, 则线程内部存放变量副本需要一个容器,而且容 器要支持快速存取, 所以在每个线程内部都可以持有一个 Map 来支持多个变量副本,这个 Map 被称ThreadLocalMap。

上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap ThreadLocalMap 是一 个声明在 ThreadLocal 的静态内部类, 然后 Thread 类中有一个这样类型成员变量,也就是
ThreadLocalMap 实例化是在 Thread 内部,所以 getMap 是直接返回 Thread 的这个成员。
看下 ThreadLocal 的内部类 ThreadLocalMap 源码,这里其实是个标准的 Map 实现,内部有一个元素类型为 Entry 的数组, 用以存放线程可能需要的多个副本变量

可以看到有个 Entry 内部静态类,它继承了 WeakReference ,总之它记录了 两个信息, 一个是
ThreadLocal<?> 类型, 一个是 Object 类型的值。 getEntry 方法 则是获取某个 ThreadLocal 对应的值,set 方法就是更新或赋值相应的 ThreadLocal 对应的值。

其实就是拿到每个线程独有的 ThreadLocalMap然后再用 ThreadLocal 的当前实例,拿到 Map 中的相应的 Entry,然后就可以拿到相应的值返回出去。当然,如果 Map 为空,还会先进行 map 的创建,初始化等工作。

Hash 冲突的解决

开放定址法:
基本思想是,出现冲突后按照一定算法查找一个空位置存放,根据算法的不同又可以分为线性探
测再散列、二次探测再散列、伪随机探测再散列。线性探测再散列即依次向后查找,二次探测再散列, 即依次向前后查找, 增量为 1 、2 、3 的二次方,伪随机,顾名思义就是随机产生一个增量位移。ThreadLocal 里用的则是线性探测再散列

链地址法:
这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的
单链表, 并将单链表的头指针存在哈希表的第 i 个单元中, 因而查找、插入和删 除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。 Java里的 HashMap 用的就是链地址法,为了避免 hash 洪水攻击,1.8 版本开始还引 入了红黑树。

再哈希法:
这种方法是同时构造多个不同的哈希函数: Hi=RH1(key) i=1 ,2 ,… ,k 当哈希地址 Hi=RH1(key)发生冲突时,再计算 Hi=RH2(key)……,直到冲突 不再产生。这种方法不易产生聚集,但增加了计算时间

下面我们分两种情况讨论:
key 使用强引用: 对 ThreadLocal 对象实例的引用被置为 null 了,但是
ThreadLocalMap 还持有这个 ThreadLocal 对象实例的强引用, 如果没有手动删除, ThreadLocal 的对象 实例不会被回收,导致 Entry 内存泄漏。
key 使用弱引用: 对 ThreadLocal 对象实例的引用被被置为 null 了,由于
ThreadLocalMap 持有 ThreadLocal 的弱引用, 即使没有手动删除, ThreadLocal 的 对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set get remove 都 有机会被回收。
比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长, 如果都没有
手动删除对应 key ,都会导致内存泄漏, 但是使用弱引用可 以多一层保障。
因此, ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏, 而不是因为弱引用。

JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。 JVM 利用调用 remove 、get、set 方法的时候,回收弱引用。当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get 、set 方法,那么将导致内存泄漏。使用线程池+ ThreadLocal 时要小心, 因为这种情况下, 线程是一直在不断的 重复运行的,从而也就造成了 value 可能造成累积的情况。

错误使用

为什么每个线程都输出 115 ?难道他们没有独自保存自己的 Number 副本吗? 为什么其他线程还
是能够修改这个值?仔细考察 ThreadLocal Thead 的代码, 我们发现 ThreadLocalMap 中保存的其实是对象的一个引用,这样的话,当有其 他线程对这个引用指 向的对象实例做修改时, 其实也同时影响了所有的线程持有 的对象引用所指向的同一个对象实例。 这也就是为什么上面的程序为什么会输出 一样的结果。 而上面的程序要正常的工作,应该的用法是让每个线程中的ThreadLocal 都 应该持有一个新的 Number 对象。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值