详解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中的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的>

dLocalMap threadlocalmap = getMap(thread);

if(threadlocalmap != null)

{

ThreadLocalMapEntry entry = threadlocalmapgetEntry(this);

if(entry != null)

首先加深印象:

1ThreadLocal解决的是每个线程需要有自己独立的实例,且这个实例的修改不会影响到其他线程。

这个ThreadLocal的用法一般都是创建各自线程自己运行过程中单独创建的对象的,不适合的相同实例共享的。

>

Handler相信安卓开发者都很熟悉了,平常在开发的时候应用场景很多,但是Handler到底是如何发送消息和接收消息的呢,它内部到底做了些什么工作呢,本篇文章就Handler来分析它的源码流程

在Handler中有多个发送消息的方法,以下为几个例子:

第一个不用多说直接发送消息的,延迟时间是0

第二个发送带有延迟的消息,如果delayMillis 是负数则设置为0

第三个发送消息排在消息队列的头部,等待处理

从源码可以看出来第一个和第二个方法都是调用sendMessageAtTime方法,而sendMessageAtTime方法调用的是enqueueMessage方法,所以它们调用的都是enqueueMessage方法

走到这里我们看到这个msgtarget 就是当前Handler,这里之所以这样写是为了后面用于消息分发的,这里的queue不会为空,我们来看queue到底在哪里实例的

在这里看到不仅MessageQueue在这里实例化并且Looper也是在这里实例化的,在这里有个疑问就是mQueue这样写会不会为空呢,带着这个疑问我们后面解答,我们先看Looper中的myLooper方法

发现是从ThreadLocal静态对象里面获取的Looper对象,再看下在哪里设置的呢

在这里我们看下ThreadLocal是怎么设置和获取的,找到set方法和get方法

在这里说明一下一个线程对应一个ThreadLocalMap 一个ThreadLocalMap 对应一个key和value值,key对应的是sThreadLocal,value对应的存储的Looper对象。接着在这里发现MessageQueue是在这里实例化的,这里有个prepare方法里面设置的,那到底在哪里调用的呢。熟悉Activity启动流程源码的童鞋都知道,最终Activity启动流程 *** 作主要是在ActivityThread里面的,并且Android程序刚开始的入口是ActivityThread的main方法,所以我们查看main方法

我们看到在入口调用了LooperprepareMainLooper方法,我们直接进入方法

在这里惊奇的发现调用了prepare方法,在下面判断中保证一个线程中只能有一个Looper对象,否则抛出异常。

走到这里做个总结:

从上面可以看出几乎所有的发送消息方法都会调用enqueueMessage方法,我们查看MessageQueue中的enqueueMessage方法

这个方法主要是用来存储消息队列的,并且通过时间进行有序排序,有了消息之后就通过nativeWake方法这个方法是底层实现的,这个方法是用通过JNI实现的,即在底层通过C实现的,底层在这里就不多说了,不是本篇主要内容。在上面我们在ActivityThread中main方法中调用了Looperloop()方法,这样这个方法就被唤醒,接着我们查看此方法到底干了啥

这个方法是个无限循环方法等待获取可以处理分发msg消息的,我们重点来看MessageQueuenext()方法

这个方法很重要在这里主要是取出Message返回给Looperloop()用做消息分发,现在来看Looperloop()方法中调用的Handler dispatchMessage方法

在这里如果Message 设置了callback 的话,则直接调用 messagecallbackrun()方法,如果有设置Handler的callback,则也进行分发,如果都没有的话,那就直接调用handleMessage(Message msg)方法。

做个总结:

最后:

在这里多说几句为什么用链表结构的方式进行存储消息,而不用数组的方式呢,熟悉ArrayList源码的开发者都知道它里面其实就是以数组的方式进行存储数据的,而LinkedList是以节点的方式存储的,相当于二叉树结构的和链表结构类似,所以最终我们知道链接结构主要是增加删除效率高,而数组的方式则是查询的效率高

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

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

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

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

发表评论

登录后才能评论

评论列表(0条)

保存