首页 > 编程知识 正文

threadlocal实现原理和用途,ThreadLocal用过么,原理是什么,用的时候要注意什么

时间:2023-05-06 18:20:25 阅读:281368 作者:528

1.ThreadLocal 是什么?

ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,适用于各个线程不共享变量值的操作。

2.ThreadLocal 工作原理是什么?

ThreadLocal 原理:每个线程的内部都维护了一个 ThreadLocalMap,它是一个 Map(key,value)数据格式,key 是一个弱引用,也就是 ThreadLocal 本身,而 value 存的是线程变量的值。

弱引用, WeakReference当一个对象只用弱引用的时候, 收集器发现它了就会回收它, 没发现就是还可以使用的

软引用, SoftReference 当内存不足的时候, 在进行fullGC之前会回收软引用, 所以使用用来作为缓存

虚引用 PhantomReference 这个引用丝毫不会影响它的回收,虚引用主要用来跟踪对象被垃圾回收器回收的活动

也就是说 ThreadLocal 本身并不存储线程的变量值,它只是一个工具,用来维护线程内部的 Map,帮助存和取变量。

这个类的结构类似, Map<线程ID, Map<ThreadLocal对象,value>> 的线程安全的静态类

3.ThreadLocal 如何解决 Hash 冲突?

ThreadLocalMap 是当前线程对象thread中成员变量, 当set数据的时候, 会获得这个成员变量, 如果获得的是空的就会new一个,

与 HashMap 不同,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocal的key是ThreadLocal的这个对象本身的弱引用类型, 实现方式是继承WeakReference<ThreadLocal<?>> 类。

线性探测源代码实现如下:

/ * Increment i modulo len. */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } / * Decrement i modulo len. */ private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } 4.ThreadLocal 的内存泄露是怎么回事?

ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被 Entry 中的 Key 引用的,因此如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。这个时候 Entry 中的 key 已经被回收,但是 value 又是 强引用不会被垃圾收集器回收,这样 ThreadLocal 的线程如果一直持续运行,value 就一直得不到回收,这样就会发生内存泄露。

既然这样使用弱引用有什么用呢?
答: 在key被回收后, map中的这个key就是null。 如果下次ThreadLocal再放入对象的时候,会检查map中key为null的值, 并把他移除掉。

5.为什么 ThreadLocalMap 的 key 是弱引用?

使用弱引用确实很巧妙, 但是使用不当还是会出现内存泄漏的

ThreadLocalMap的Entry中的key是弱引用,当threadLocal外部强引用被置null(ThreadLocal instance=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。

在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的,所以,threadLocal的内存泄漏问题,是应该值得我们思考和注意的问题。

我们知道 ThreadLocalMap 中的 key 是弱引用,而 value 是强引用才会导致内存泄露的问题,至于为什么要这样设计,这样分为两种情况来讨论:

key 使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。key 使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。

比较以上两种情况,我们可以发现:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。

6.ThreadLocal 的应用场景有哪些?

mybatis的与spring的结合就是使用的ThreadLocal. 会将数据库连接放入ThreadLoacl中, 当运行某个查询方法的时候, 在AOP的before中获得连接, 并且根据情况开启事务, 在最后after的时候commit连接, 这样用户就可以实现无感知的"不调用"的jdbc来达到操作数据库的目的

ThreadLocal 适用于独立变量副本的情况,比如 Hibernate 的 session 获取场景。

示例代码:

private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>(); public static Session getCurrentSession(){ Session session = threadLocal.get(); try { if(session ==null&&!session.isOpen()){ //... } threadLocal.set(session); } catch (Exception e) { // TODO: handle exception } return session; } set方法

与concurrentHashMap,hashMap等容器一样,threadLocalMap也是采用散列表进行实现的。在了解set方法前,我们先来回顾下关于散列表相关的知识(摘自这篇的threadLocalMap的讲解部分以及这篇文章的hash)。

散列表

理想状态下,散列表就是一个包含关键字的固定大小的数组,通过使用散列函数,将关键字映射到数组的不同位置。在理想状态下,哈希函数可以将关键字均匀的分散到数组的不同位置,不会出现两个关键字散列值相同(假设关键字数量小于数组的大小)的情况。但是在实际使用中,经常会出现多个关键字散列值相同的情况(被映射到数组的同一个位置),我们将这种情况称为散列冲突。为了解决散列冲突,主要采用下面两种方式: 分离链表法(separate chaining)和开放定址法(open addressing)

分离链表法

分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。当查询的时候,首先找到元素所在的链表,然后遍历链表查找对应的元素,典型实现为hashMap,concurrentHashMap的拉链法。下面是一个示意图:

开放定址法

开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 – 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)。

ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。

在了解这些相关知识后我们再回过头来看一下set方法。set方法的源码为:

private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; //根据threadLocal的hashCode确定Entry应该存放的位置 int i = key.threadLocalHashCode & (len-1); //采用开放地址法,hash冲突的时候使用线性探测 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //覆盖旧Entry if (k == key) { e.value = value; return; } //当key为null时,说明threadLocal强引用已经被释放掉,那么就无法 //再通过这个key获取threadLocalMap中对应的entry,这里就存在内存泄漏的可能性 if (k == null) { //用当前插入的值替换掉这个key为null的“脏”entry replaceStaleEntry(key, value, i); return; } } //新建entry并插入table中i处 tab[i] = new Entry(key, value); int sz = ++size; //插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

作者:你听___

链接:https://www.jianshu.com/p/30ee77732843

来源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。