ThreadLocal篇

关于threadlocal

threadlocal的实现

ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。

threadlocal的内部结构

以前的设计

每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法在这里插入图片描述

jdk8 开始

每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。

  1. Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
  2. ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal(它的一个弱引用),value为代码中放入的值。
  3. 每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
  4. 无论是 get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。只有 remove() 方法中显式调用了 expungeStaleEntry 方法。

在这里插入图片描述

jdk8 优化后的好处

  1. 每个Map存储的Entry数量就会变少,也减少了hash冲突。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。
  2. 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

synchronized 与 threadlocal 的区别?

synchronizedthreadlocal
原理加锁,排队访问(时间换空间)为每个线程复制一份变量副本(空间换时间)
侧重点多线程间资源的同步多线程中数据隔离

ThreadLocal.set()

  1. 首先获取当前线程,并根据当前线程获取一个Map
  2. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
  3. 如果Map为空,则给该线程创建 Map,并设置初始值

ThreadLocal.get() 的简单理解

  1. 首先获取当前线程, 根据当前线程获取一个Map
  2. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到4
  3. 如果e不为null,则返回e.value,否则转到4
  4. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

ThreadLocalMap

ThreadLocalMap 的实现

类似hashmap,但没有实现map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。

threadLocal的内存泄漏问题

  1. key以弱引用的方式指向threadlocal.
  2. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收,key也被回收了.
  3. 但是我们的value却不能回收,因为存在一条从current thread连接过来的强引用.只有当thread结束以后,强引用断开, value才会被GC回收。
  4. 所以当使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。

解决办法

  1. 避免key被自动回收,使用static修饰。 使用static final修饰threadLocal保留一个全局的threadLocal方便传递其他value(threadLocal一直被强引用)。这样就不会让gc回收作为key的threadLocal。即不会导致key为null。
  2. 手动执行remove()。使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

为什么使用弱引用而不是强引用?

使用强引用

在这里插入图片描述

  1. 假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
  2. 但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
  3. 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。

​ 也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。

使用弱引用

在这里插入图片描述

  1. 同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
  2. 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
  3. 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

​ 也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

出现内存泄漏的真实原因
  1. 没有调用 remove方法
  2. thread 线程不会被回收
为什么要使用弱引用

​ 事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value回收。

​ 这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

ThreadLocalMap的 Hash 算法

下标计算方式:

int i = key.threadLocalHashCode & (len-1);

每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长HASH_INCREMENT( 0x61c88647 )。这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。

ThreadLocalMap的 Hash 冲突以及处理

  1. 虽然ThreadLocalMap中使用了黄金分割数来作为hash计算因子,大大减少了Hash冲突的概率,但是仍然会存在冲突。HashMap中解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树。
  2. 而 ThreadLocalMap 中并没有链表结构,所以这里不能使用 HashMap 解决冲突的方式了。而是使用线性探测法。当发生hash冲突时,就会线性向后查找,一直找到 Entry 为 null 的槽位才会停止查找,将当前元素放入此槽位中。若整个空间都找不到空余的地址,则产生溢出。可以把Entry[] table看成一个环形数组。溢出就会执行到后面的清理空闲槽位,条件满足时就会rehash();

ThreadLocalMap.set()详解

  1. 首先还是根据key计算出索引 i,然后查找i位置上的Entry,
  2. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
  3. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
  4. 不断循环检测,直到遇到为null的。
  5. 如果循环结束都没有return,那么就需要新建一个Entry,并且插入,同时size增加1。
  6. 最后调用cleanSomeSlots,清理key为null的Entry,再判断sz 是否达到了rehash的条件((数组长度的 2/3),达到的话就会调用rehash函数执行一次全表的扫描清理。
  7. rehash()中会先进行一轮探测式清理,清理过期key,清理完成后如果size >= threshold - threshold / 4,就会执行真正的扩容逻辑

ThreadLocalMap过期 key 的清理流程

探测式清理流程(expungeStaleEntry)

遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的Entry设置为null,沿途中碰到未过期的数据则将此数据rehash后重新在table数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null的桶中,使rehash后的Entry数据距离正确的桶的位置更近一些。

启发式清理流程(cleanSomeSlots)

在添加新元素或删除另一个陈旧元素时调用。它执行对数次扫描,循环 log2n次,如果在循环中发现了 过期key,此时会进行探测式清理,并重置循环次数为 log2n次

ThreadLocalMap的扩容

在 ThreadLocalMap.set() 方法的最后,启发式清理未删除任何条目,且散列数组中条目的数量已经达到扩容阈值,就开始执行 rehash() 逻辑。

  1. 探测式清理所有过期数据,如果依然不能缩小表空间,则扩容
  2. size >= threshold * 3/4,时执行resize
  3. 扩容后的tab的大小为原理两倍,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的entry为null的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中,并重新计算tab下次扩容的阈值。

InheritableThreadLocal

解决子线程无法共享父线程中创建的线程副本数据问题。实现原理是子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init方法在Thread的构造方法中被调用。在init方法中拷贝父线程数据到子线程中。但InheritableThreadLocal仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。

当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个TransmittableThreadLocal组件就可以解决这个问题

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocalJava中一个非常重要的线程封闭技术。它可以让每个线程都拥有自己的变量副本,避免了线程间的竞争和数据泄露问题。在本文中,我们将详细介绍ThreadLocal的定义、用法及其优点。 1. ThreadLocal的定义 ThreadLocalJava中一个用来实现线程封闭技术的类。它提供了一个本地线程变量,可以在多线程环境下使每个线程都拥有自己的变量副本。每个线程都可以独立地改变自己的副本,而不会影响到其他线程的副本。ThreadLocal的实现是基于ThreadLocalMap的,每个ThreadLocal对象都对应一个ThreadLocalMap,其中存储了线程本地变量的值。 2. ThreadLocal的用法 使用ThreadLocal非常简单,只需要创建一个ThreadLocal对象,然后调用其get()和set()方法即可。get()方法用来获取当前线程的变量副本,如果当前线程还没有变量副本,则会创建一个新的副本并返回。set()方法用来设置当前线程的变量副本,如果当前线程已经有了变量副本,则会覆盖原来的副本。 下面是一个简单的例子,演示了如何使用ThreadLocal来实现线程封闭: ```java public class ThreadLocalTest { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { new Thread(() -> { threadLocal.set("Thread A"); System.out.println("Thread A: " + threadLocal.get()); }).start(); new Thread(() -> { threadLocal.set("Thread B"); System.out.println("Thread B: " + threadLocal.get()); }).start(); Thread.sleep(1000); System.out.println("Main: " + threadLocal.get()); } } ``` 运行结果如下: ``` Thread A: Thread A Thread B: Thread B Main: null ``` 从输出结果可以看出,每个线程都拥有自己的变量副本,互不影响。而在主线程中,由于没有设置过变量副本,所以返回null。 3. ThreadLocal的优点 ThreadLocal的优点主要体现在以下几个方面: (1)线程安全:ThreadLocal可以避免线程间的竞争和数据泄露问题,每个线程都可以独立地修改自己的变量副本,不会影响其他线程。 (2)高效性:ThreadLocal使用起来非常简单,而且性能也非常高,比如在Web开发中,可以将用户信息存储在ThreadLocal中,从而避免在每个方法中都去查询数据库。 (3)易用性:ThreadLocal的使用非常灵活,可以根据实际需要自由地定义数据类型和访问方式。 总的来说,ThreadLocalJava中一个非常重要的线程封闭技术,可以帮助开发人员避免线程间的竞争和数据泄露问题,提高程序的安全性和性能。在实际开发中,我们应该充分利用ThreadLocal的优点,合理地运用它来解决各种线程安全问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值