面试handler系列:
面试 - handle使用及原理(1)
面试 - handle之详谈Message(2)
回顾Looper知识点
在第一篇中 面试 - handle使用及原理(1) 在讲到Looper
部分源码的时候,Looper
生成跟返回有以下源码:
可以看到Looper
的对象是放到sThreadLocal
里面的,sThreadLocal
保持只有一个Looper
对象。我们知道,Handler
需要获取当前线程中的Looper
对象,Looper.loop()
是作用于当前线程,并且不同线程拥有的Looper
对象不同。使用ThreadLocal
对Looper
进行保存,那就实现了在不同的线程中读取到的Looper
对象就是相应的那个线程中的。
这里的ThreadLocal
是何方神圣,可以保持每个线程中拥有独立的Looper
对象?
ThreadLocal的定义
ThreadLocal 是 JDK底层提供的一个解决多线程并发问题的工具类,它为每个线程提供了一个本地的副本变量机制,实现了和其它线程隔离,这种变量只在本线程的生命周期内起作用。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
言简意赅: ThreadLocal是线程内部的数据存储类,自己线程存储的数据只有自己才能获取到,其它线程获取不到。
####ThreadLocal例子
根据上面的结果可以看到,同一个mThread
对象,不同线程中读取出来的信息是不同的,读取的信息都是自己线程下设置的信息。
为什么会有这种效果? 这个咱们就得来看看实现的原理了
ThreadLocal原理
分析原理的话,我们从mThread.set()
方法开始为切入点吧
set()方法
从上面的源码可以看到 关键代码1:传入线程的当前线程,并返回一个ThreadLocalMap
类型的map
对象 关键代码2:判断map
是否为null
不为null
则传入ThreadLocal
跟value
, 为null
则创建map
并传入线程对象t
跟value
先来看一下createMap(t, value)
方法
可以看到实际上存储到ThreadLocal
中的数据是存到ThreadLocalMap
中,这个一看就是HashMap
的类似数据结构,key
传入的是this
对象是ThreadLocal
类型的,value
是泛型,我们自己定义的一个数据类型。
ThreadLocalMap类
接下来看 ThreadLocalMap
类的大致一些信息
ThreadLocalMap
中可以看到Entry
是存储单位,而存储结构实际上是一个Entry[] table
数组。 上面的构造函数可以看到类似hashmap
先用Key
值进行hash算法计算映射,求出位于数组的下标的多少,然后将传入的key
跟value
值初始化一个Entry
对象,并放到Entry[] table
中。然后设置阈值 len*2/3 (len = 16)
如果超过阈值便要重新分配table
Entry
的数据类中继承了WeakReference<ThreadLocal<?>>
使用弱引用挟持着ThreadLocal防止挟持线程的数据,导致线程生命周期受到影响进而导致内存泄漏
这里简要说明一下常用的几个引用的作用 强应用:这个是我们常用的平时都是new Object(),就算是OOM都不会被虚拟机回收 软引用: new SoftReference() ,当要发生OOM的时候会被回收掉 弱引用:new WeakReference(),当GC的时候就会被回收掉
返回最开始的mThread.set()
方法里面的map.set(this, value)
上面的简单讲解一下 如果传入的key计算出下标,根据下标有三种情况处理:
- tab[i]为null :直接插入新的数据,如果
sz >= threshold
则重新扩增数组rehash()
2.tab[i]不为null分下面两种情况
- key不为null :则更新新的数据
e.value = value
并返回 - key为null:则调用
replaceStaleEntry(key, value, i)
将数据存储进去,并返回
最主要看下面的几个方法cleanSomeSlots
cleanSomeSlots
主要就是将那些key不为null,但是value为空的调用expungeStaleEntry(i)
清除掉,如果清除了就是返回ture
否则返回false
expungeStaleEntry(i)
是真正删除数据的地方,删除那些key
存在,但是value
不存在的Entry
数据
rehash()
就是先调用expungeStaleEntry()
,然后再根据if (size >= threshold - threshold / 4)
去决定要不要resize()
resize()
这个就是现在的关键了
简单的解释一下上面的源码:
关键代码1:生成新的数组,新的数组是旧的数据的两倍
关键代码2:轮训旧数据,将e!= null
的Entry
取出 如果key
为null,则清除掉对应的value
如果key
不为null,则计算出对应的新的数组的下标 并存储进去
关键代码3:设置新的阈值。
get 方法
先看一下没有数据的时候返回什么的吧
看见了吗,返回了一个空值,并向ThreadLocalMap
中插入(key=this,value = null)
那有数据的时候ThreadLocalMap.Entry e = map.getEntry(this)
看一下源码
计算出数组下标,然后比较当前的key
是不是要找的那个 ThreadLocal
,如果不是则调用 getEntryAfterMiss(key, i, e)
从当前节点开始线性查找。
有便返回没有便返回空,发现k==null
的时候再调用了expungeStaleEntry(i)
去掉该元素。
总结: 1.每个线程都存在
ThreadLocalMap
,在线程中通过ThreadLoad<T>
获取到对应线程下的ThreadLocalMap
然后在里面传入ThreadLoad
作为键去进行数据的存储跟读取。
自己实现类似功能
注意事项
- 声明为全局静态的final成员。
添加的时候是以threadLocal为key,传入的值为value的。所以,不同的threadLocal对象会导致存储的数据对象不一致而导致找不到数据。
- 避免存储大量对象。
- 用完之后及时移除对象。
关于ThreadLocal的就到这里,如果有什么疑问或者面试碰到的问题欢迎在评论区留言