详解ThreadLocal的设计思想和扩容机制

详解ThreadLocal的设计思想和扩容机制,第1张

对于共享资源,在多线程环境下势必会存在线程安全的问题。主要有两种方式可以处理这个问题,一种,就是将资源处理变成单线程,同一时刻只能有一个线程持有,比如加锁。另一种,用空间换时间的思想,将共享资源变成非共享,比如每个线程都各自持有一份,线程只 *** 作自己持有的资源,那样就不存在线程安全的问题。ThreadLocal就是采用了第二种思想实现。

将一个共享资源定义成ThreadLocal,JDK18 ThreadLocal提供了withInitial很方便地去声明一个线程的局部变量,以SimpleDateFormat为例,具体用法:

先看一下Thread类,内部定义了一个ThreadLocalThreadLocalMap,用来存放线程自身持有的变量。查看源码,可以看到对于ThreadLocalMap的定义:存放与该线程有关的ThreadLocal值,这个Map是由ThreadLocal去维护。可见Thread类只是定义了这么一个东西,但是却不去维护里面的值,有点不负责任的样子。

回到ThreadLocal这个类,对于Map的维护无非就是数据的写入和读取。从get()可以看到,会先获取当前的线程,从当前线程拿到ThreadLocalMap,从Map中获取当前的ThreadLocal对象(以上面为例,就是获取LOCAL_FORMAT),如果不为空就返回对应的值,如果Map是空或者获取不到当前ThreadLocal对象,那么就会进行初始化。

setInitialValue()里同样去获取当前线程里面的Map,如果没有,调用createMap去初始化Map。

继续跟踪源码,可以看到最后调用的是ThreadLocalMap的构造函数,创建了一个Entry数组,初始值是16,初始大小是1,设置阈值是当前大小的2/3,同时计算了第一个元素的位置。

再看一下set()方法,也是获取当前线程的ThreadLocalMap,如果不为空,就调用ThreadLocalMapset()添加当前的值,如果为空,则去创建Map。

继续看ThreadLocalMapset()方法,通过key和hashCode去计算当前存放的位置,如果当前位置位置已存在元素,则判断key是否相同,相同则覆盖,不相同判断是否为null,如果为null,则说明原先所在的key被回收了,此时会替换旧值。如果找不到,则说明当前位置已经被其他人占用了,则会往后继续找合适的位置存放。其实就是开放寻址的思想。

根据上图可以看到,添加了元素后,会判断当前下标往后的元素是否存在需要value回收的,如果不存在需要回收并且当前总元素大于等于阈值,那么就会调用rehash()扩容。扩容之前还会再重新对key已经被GC了的元素进行回收,回收后如果总数还是大于等于阈值的3/4,那么就会调用resize()进行真正的扩容。

查看扩容源码,可以看到扩容后的数组是原来数组的2倍,原有的元素会重新进行hash计算放到新的数组里面,处理完后还会重置阈值。

ThreadLocal解决了多线程下共享资源并发的问题,但也存在其他问题。

看一下Entry的结构,继承了WeakReference,可见ThreadLocalMap的key是弱引用。在JVM里弱引用生命周期是比较短的,JVM扫描一旦发现有弱引用,无论当前内存空间是否足够,都会进行回收。而ThreadLocal的value是强引用,这样一来key会被清除掉,而value不会被清除掉,如果不做任何处理,value永远无法被回收,这个时候就可能会产生内存泄露。

而ThreadLocal也考虑到了这个问题,因此在get()、set()、remove()方法里都提供对key为null数据的处理,最终都是调用expungeStaleEntry进行重新散列来清除key为空的数据。

所以最好每次使用后都调用remove()方法。其实也在想,为什么ThreadLocalMap的key不定义成强引用,那样就可以减少维护Map的代码。后来想了下,这里可能也是为了实现即使没有显示调用remove()也可以回收不怎么经常被用到对象,毕竟如果线程比较多的话,相当于会有多个相同的对象存在内存里,如果忘记显示调用remove(),对象常驻内存也是种压力,更多算是一种兜底的考虑吧。

ThreadLocal 是一种线程安全的 数据容器 ,实现原理就是会在不同的线程保持单独存储变量。

最简单的使用方式如下:

构造方法如下:

ThreadLocalMap 是 ThreadLocal 内部的一个类,类似 HashMap,如下:

其中其 Entry 继承自 WeakReference 中,每次存储一个值,都会把值的 HashCode 作为 作为key,并且这个 key 是一个 WeakReference 对象。

总结一下 ThreadLocalMap 存储的是一个数组,每个数组的元素如下:

其中 key 是当前 ThreadLocal 对象的 Hash ,value 是 ThreadLocal 对象的存储的值。

set()方法把一个对象/变量存储到特定线程的内存里面,如下:

其中 get() 方法,会先从当前线程对象活动 ThreadthreadLocals 对象,它是一个 ThreadLocalMap 对象。简单来说,就是不同的线程保持单独保存自己线程的变量。

get()方法用于获取当前线程的变量,如下:

通用的会先获取 会先从当前线程对象活动 ThreadthreadLocals 对象,然后从 ThreadLocalMap 对象获取当前线程存储的变量。

线程安全,不需要多线程交互,各自线程单独运行。例如 Looper,如下:

ThreadLocal 使用可能会导致内存泄露,因为 ThreadLocalMap的 Entry 的key(对应该ThreadLocal hash) 是 WeakReference 类型,可能在 GC的是会回收。也就是 ThreadLocal 存入到 ThreadLocalMap 之后,如果 key 被GC 回收,这个ThreadLocal 对象保存的内容,将永远无法被使用,并且由于线程还存活,所以ThreadLocalMap 不会被销毁,最终导致 ThreadLocal 的内容一直在内存里。

但是设计者在设计上避免了这个问题,就是当你再次调用 get() remove() set() 方法的时候,会自动清理 key 为null 的对象。

在使用完成之后,调用 remove() 方法。

ThreadLocal的作用是什么?使用时有哪些注意事项?为什么ThreadLocalMap中的Entry要使用WeakReference?netty中FastThreadLocal又做了什么优化? 答案尽在本文中。

用ThreadLocal修饰的变量,一般我们称为线程本地变量。那么一般什么情况下会使用ThreadLocal呢?

ThreadLocal提供的方法有get(),set(T value), remove(),并且有一个可以override的initialValue()方法。

一般我们定义ThreadLocal变量都定义成static final的变量,然后就可以通过这个ThreadLocal变量进行get set了。

了解了ThreadLocal的作用后,我们开始分析一下内部实现。

首先,每个线程Thread对象内有一个ThreadLocalMap类型的threadLocals字段,里面保存着key为ThreadLocal到value为Object的映射。

ThreadLocal的get,set,remove方法都是通过 *** 作当前线程内的ThreadLocalMap字段实现的, *** 作的key为自己(this)

上面的ThreadLocal的实现中大部分都是对ThreadLocalMap的 *** 作封装,那么ThreadLocalMap是怎么实现的呢? ThreadLocalMap是ThreadLocal类的静态内部类。

ThreadLocalMap和HashMap有所不同,ThreadLocalMap使用线性探查法而不是拉链法解决hash冲突问题。

线性探查法可以用一个小例子来理解,想象一个停车场的场景,停车场中有一排停车位,停车时,会计算车子的hashCode算出在停车位中的序号,停上去,如果那个车位有车了, 则尝试停到它的下一个车位,如果还有车则继续尝试,到末尾之后从头再来。当取车时,则按照hashCode去找车,找到对应的位置后,要看一下对应的车位上是不是自己的车,如果不是, 尝试找下一个车位,如果找到了自己的车,则说明车存在,如果遇到车位为空,说明车不在。要开走车时,不光是简单开走就可以了,还得把自己车位后面的车重新修改车位,因为那些车可能因为 hash冲突更换了位置,修改车位的范围是当前位置到下一个为空的车位位置。当然还有扩容的情况,后面代码里会具体介绍。

那么为什么使用线性探测法而不是链表法呢?主要是因为数组结构更节省内存空间,并且一般ThreadLocal变量不会很多,通过0x61c88647这个黄金分割的递增hashCode也能比较好的分布在数组上减少冲突。

Map中的元素用一个Entry类表示,Entry包含了对ThreadLocal的WeakReference,以及对ThreadLocal值的强引用。

下面用一段代码来阐述一下。

我们创建了一个People类,并且创建了一个类型为ThreadLocal的实例字段,我们在main方法中连续调用say()方法,会发现打印出来的threadLocal的值是不一样的,虽然我们这些调用都在 同一个线程中,但是因为每次调用的ThreadLocal对象是不同的,也就是ThreadLocalMap的key不相同。如果我们把ThreadLocal字段加上static,就会发现打印出来的都是相同的值了。 长时间运行的线程是有可能出现的,比如tomcat的>

tthreadLocals 是当前线程Thread(t) 的成员变量, 当使用 ThreadLocal 创建对象后,调用 ThreadLocalset()方法会看到初始化 ThreadLocalMap的过程,JDK内部实现代码截图如下:

(1)调用set方法,初始化 ThreadLocalMap 对象,如果getMap(t)获取当前线程 threadLocals 变量为空,随后创建一个;反之,直接使用存储线程数据

(2)创建ThreadLocalMap 对象的方法实现,即为当前线程 threadLocals赋值

以上就是关于详解ThreadLocal的设计思想和扩容机制全部的内容,包括:详解ThreadLocal的设计思想和扩容机制、Android 线程安全-ThreadLocal、ThreadLocal里的巧妙设计、常见面试问题等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9569601.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-29
下一篇 2023-04-29

发表评论

登录后才能评论

评论列表(0条)

保存