ThreadLocal 是一个线程变量工具类,可以提供线程局部变量
-
ThreadLocal可以为每个线程提供线程局部变量,变量对其他线程而言是隔离的。
-
为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。
ps : 可以通过重新 initValue()来自定义初始值
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是
- Synchronized是通过线程等待,牺牲时间来解决访问冲突
- ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
ThreadLocal是用冗余的方式换时间,而锁机制则是时间换空间。
3.应用场景(能干什么?)- 线程间数据隔离,各线程的 ThreadLocal 互不影响
- 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
- Spring 事务管理器采用了 ThreadLocal
- Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
- 一些ORM框架的Session管理,web系统的会话管理。
- 使用到类似责任链模式,给每个方法增加一个context上下文参数非常麻烦,可以使用 ThreadLocal传递。
简单总结:
一个ThreadLocal只能保存一个变量的副本,如果需要多个,就得创建多个变量;我们确定使用完需要执行remove避免内存泄漏。
- 可通过 ThreadLocal value = new ThreadLocal(); 来使用。
- 会自动在每一个线程上创建一个 T 的副本,副本之间彼此独立,互不影响,可以用 ThreadLocal 存储一些参数,以便在线程中多个方法中使用,用以代替方法传参的做法。
- 每个线程对象 Thread 对象内部拥有一个 threadLocals 字段,这个属性会指向一个Thead对象中ThreadLocalMap;
- ThreadLocalMap 存储的是当前线程与其它 ThreadLocal关联的数据
- 线程访问某个ThreadLocal对象的get或set 方法的时候都会检测,在get方法获取的时候会判断当前线程的Map 是否有key , 为这个 ThreadLocal对象的Entry数据
- 若没有 ThreadLocal的initialValue方法会创建一个Entry数据并放入到ThreadLocalMap中
- 每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。
- ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
- 在ThreadLocal维护一个大Map,所有的线程变量都维护在一个Map里面。
- 线程多,若只维护一个大Map 数据过大不利于维护
- 新版本线程间数据隔离,每个线程有自己的ThreadLocalMap ,当线程被销毁的时候,就可以在下一次GC回收。
- ThreadLocalMap中 Entry对应的key是一个弱引用,如果ThreadLocal对象被回收的话,线程还未结束,ThreadLocalMap还未被回收,(ps:弱引用不参与root 算法)
- ThreadLocalMap的 key是特定类型就是 ThreadLocal对象
- key 是弱引用,HashMap key 是强引用, 弱引用不影响对象被回收。
1610.大小为什么必须是2的次方数?
- 与HashMap一样为了方便Hash寻址
- 因为2的次方数-1之后转换为二进制一定是 1111结尾的二进制数。
- 若数值与这种二进制数进行 按位 与运算得到的一定是 >=0 且 <= 这个二进制数值的 。比使用取模算法 ,效率高很多
- Entry 数组长度的 三分之二
- 比如:初始化是16 ,阈值则是 10
不会
- 会先清理过期数据(cleanSomeSlots方法)将key为null的数据移出掉。
- 若移除后当前map实际容量大小仍然阈值的 >= 75% 则扩容
- 源码技术公式 size >= threshold - threshold / 4 例如 当前大小 >= 10-10/4
- 首先会创建一个新的数组,长度是当前散列表数组的2倍
- 遍历老的数组,将其中的数据重新安装Hash算法放入新的数组
- 更新ThreadLocalMap对象的散列表引用指向新的数组
- 扩容完成之后,重新计算一下,下次触发扩容的阈值(新数组大小的 2/3 )
- 首先Get方法传进来的是ThreadLocal对象,
- 根据ThreadLocal的hash值与运算当前数组长度-1,得到下标位置index
- 若当前位置数据不为空并且key与当前查询的key相同的话返回。
- 若不是则表示遇到过Hash冲突。
- ThreadLocaMap 内部没有链表结构,若Hash冲突,是线性的存到后面某个位置。
- 所以get操作的时候,若第一次没有命中,需要后续查询,直到查到数据或者返回null
- 触发一次“探测式”过期数据回收逻辑
- 从当前桶位节点,向后迭代,将碰到key==null 会把 Entry值置位null
- 若碰到正常数据会根据key重新计算出来一个index ,看计算出来的index是否等于当前位置,
- 若相等,则啥也不做
- 若不相等,说明存数据发生过Hash冲突 。位置被占用了
- 会继续向计算出来的下标后查找一个不为null的位置存。
- ThreadLocalMap内存储的是Entry, Entry有key 和value, key 是弱引用。
- key 限定是ThreadLocal对象,value是具体的关联数据。
- 当 key对应的ThreadLocal被回收后,key 是弱引用,key.get()==null 就代表是过期数据
- 从当前线程对象中取TheadLocalMap。
- 若取不到-则创建ThreadLocalMap并存储初始化值。
- 首先根据key, hashCode 找到对应的位置。
- 若当前值为 null,则表示是新添加数据,只需要添加到这个节点。
- 若不为 null
- 说明发生hash冲突,线性向数组后。查找到一个目前没有占有的位置插入。
- 判断当前Key是否与set的 key一致,是则更新
- 若遇到 key 为空的记录 -执行清理过程,过期数据就替换
- 从当前线程对象中取TheadLocalMap。
- 若不为空 则移出以当前 ThreadLocal对象为key的数据
- 若为空,则什么也不做
- 将Entry的Key设置成弱引用,因为在配合线程池使用的情况下可能会有内存泄露的风险。
- 设计成弱引用的目的是为了更好地对ThreadLocal进行回收,
- 将ThreadLocal的强引用置为null后,这时候Entry中的ThreadLocal理应被回收了,
- 如果Entry的key被设置成强引用则该ThreadLocal就不能被回收,这就是将其设置成弱引用的目的。
那为什么ThreadLocalMap的key要设计成弱引用?
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
- ThreadLocal 在没有外部强引用时, GC 时会被回收。
- 其实同一个线程也创建了多个ThreadLocal ,共享一个 ThreadLocalMap,ThreadLocal使用完被回收。
- ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry值 又被 threadLocalMap 对象引用
- 若线程未执行完ThreadLocalMap 还会存在
- 若线程Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收
在使用完 ThreadLocal 变量后,尽量手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收
23.ThreadLocalMap如何解决哈希冲突- 如果使用HashCode来计算Hash值是不均匀的,
- ThreadLocalMap是使用了一种黄金分隔数的方式,来分配Hash,映射到散列表内很均匀。
- 比如数组长度是16,连续分隔可能是 table[0] table[4] table[8] 这样类似比较均匀。
ThreadLocal中的hash 计算底层
- AtomicInteger 的 getAndAdd方法,参数是个固定值 0x61c88647。结构非常只用一个数组存储,并没有链表结构,
- 当出现Hash冲突时采用线性查找的方式,线性查找就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。如果产生多次hash冲突,处理起来就没有HashMap的效率高,为了避免哈希冲突,使用尽量少的threadlocal变量
如果我想共享线程的ThreadLocal数据怎么办?
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
如何实现的呢?
Thread源码中,初始化Thread.init方法,将父级的值做了传递。
10.1注意空指针 (若还未set就 get 默认返回null ,可通过重写 initialValue 自定义默认初始化值)
示例:
public static ThreadLocalmyThreadLocal = new ThreadLocal () { @Override protected String initialValue() { return "This is the initial value"; } };



