“文章相关内容已授权『郭霖』公众号发布
”
前言
很高兴遇见你~ 欢迎阅读我的文章。
本文是系列文章的第二部分,主要内容是介绍Handler的内部模式结构与详解ThreadLocal。
“Handler系列文章共分为6个部分:
- 第一部分:系列开篇,从0开始认识Handler;
- 第二部分:介绍Handler内部模式,详解关键类ThreadLocal;
- 第三部分:解析Handler内部关键类:Message、MeesageQueue、Looper;
- 第四部分:解析Handler内部关键类:Handler,同时介绍HandlerThread;
- 第五部分:总结Handler,从源码设计角度思考Handler;
- 第六部分:Handler常见问题解答;
本文为系列文章的第二部分。读者可前往笔者主页选择感兴趣的部分阅读。
”
那么,我们开始吧。
正文
Handler内部模式结构
经过前面的介绍对于Hadnler机制已经有了一定的认知,但可能对他内部的模式还不太清楚。这一部分先讲解Handler的大概内部模式,目的是为下面的详解做铺垫,为做整体概念感知。先上图:
Handler机制内部有三大关键角色:Handler,Looper,MessageQueue。其中MessageQueue是Looper内部的一个对象,MessageQueue和Looper每个线程有且只有一个,而Handler是可以有很多个的。他们的工作流程是:
- 用户使用线程的Looper构建Handler之后,通过Handler的send和post方法发送消息
- 消息会加入到MessageQueue中,等待Looper获取处理
- Looper会不断地从MessageQueue中获取Message然后交付给对应的Handler处理
这就是大名鼎鼎的Handler机制内部模式了,说难,其实也是很简单。
ThreadLocal
概述
“ThreadLocal是Java中一个用于线程内部存储数据的工具类。
”
ThreadLocal是用来存储数据的,但是每个线程只能访问到各自线程的数据。我们一般的用法是:
ThreadLocal<String> stringLocal = new ThreadLocal<>();
stringLocal.set("java");
String s = stringLocal.get();
不同的线程之间访问到的数据是不一样的:
public static void main(String[] args){
ThreadLocal<String> stringLocal = new ThreadLocal<>();
stringLocal.set("java");
System.out.println(stringLocal.get());
new Thread(){
System.out.println(stringLocal.get());
}
}
结果:
java
null
线程只能访问到自己线程存储的数据。
ThreadLocal的作用
ThreadLocal的特性适用于同样的数据类型,不同的线程有不同的备份情况,如我们这篇文章一直在讲的Looper。每个线程都有一个对象,但是不同线程的Looper是不一样的,这个时候就特别适合使用ThreadLocal来存储数据,这也是为什么这里要讲ThreadLocal的原因
ThreadLocal内部结构
ThreadLocal的内部机制结构如下:
每个Thread,也就是每个线程内部维护有一个ThreadLocalMap,ThreadLocalMap内部存储多个Entry。Entry可以理解为键值对,他的本质是一个弱引用,内部有一个object类型的内部变量,如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry是ThreadLocalMap的一个静态内部类,这样每个Entry里面就维护了一个ThreadLocal和ThreadLocal泛型对象。每个线程的内部维护有一个Entry数组,并通过hash算法使得读取数据的速度达到O(1)。由于不同的线程对应的Thread对象不同,所以对应的ThreadLocalMap肯定也不同,这样只有获取到Thread对象才能获取到其内部的数据,数据就被隔离在不同的线程内部了。
ThreadLocal工作流程
那ThreadLocal是怎么实现把数据存储在不同线程中的?先从他的set方法入手:
TheadLocal.class
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
逻辑不是很复杂,首先获取当前线程的Thread对象,然后再获取Thread的ThreadLocalMap对象,如果该map对象不存在则创建一个并调用他的set方法把数据存储起来。我们继续看ThreadLocalMap的set方法:
ThreadLocalMap.class
private void set(ThreadLocal<?> key, Object value) {
// 每个ThreadLocalMap内部都有一个Entry数组
Entry[] tab = table;
int len = tab.length;
// 获取新的ThreadLocal在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();
// 如果数据存在且相同则直接返回
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 若当前位置没有其他元素则直接把新的Entry对象放入
tab[i] = new Entry(key, value);
int sz = ++size;
// 判断是否需要对数组进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这里的逻辑和HashMap是很像的,我们可以直接使用HashMap的思维来理解ThreadLocalMap:ThreadLocalMap的key是ThreadLocal,value是ThreadLocal对应的泛型。他的存储步骤如下:
- 根据自身的threadLocalHashCode与数组的长度进行相与得到下标
- 如果此下标为空,则直接插入
- 如果此下标已经有元素,则判断两者的ThreadLocal是否相同,相同则更新value后返回,否则找下一个下标
- 直到找到合适的位置把entry对象插入
- 最后判断是否需要对entry数组进行扩容
是不是和HashMap非常像?和HashMap的不同是:hash算法不一样,以及这里使用的是开发地址法,而HashMap使用的是链表法。ThreadLocalMap牺牲一定的空间来换取更快的速度。具体的Hash算法这里就不再深入了,有兴趣的读者可以阅读这篇文章ThreadLocal传送门
然后继续看ThreadLocal的get方法:
ThreadLocal.class
public T get() {
// 获取当前线程的ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 根据ThreadLocal获取Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果没找到也会执行初始化工作
if (e != null) {
@SuppressWarnings("unchecked")
// 把获取到的对象进行返回
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
前面讲到ThreadLocalMap其实非常像一个HashMap,他的get方法也是一样的。使用ThreadLocal作为key获取到对应的Entry,再把value返回即可。如果map尚未初始化则会执行初始化操作。下面继续看下ThreadLocalMap的get方法:
ThreadLocalMap.class
private Entry getEntry(ThreadLocal<?> key) {
// 根据hash算法找到下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 找到数据则返回,否则通过开发地址法寻找下一个下标
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
利用ThreadLocal的threadLocalHashCode得到下标,然后根据下标找到数据。没找到则根据算法寻找下个下标。
内存泄露问题
我们会发现Entry中,ThreadLocal是一个弱引用,而value则是强引用。如果外部没有对ThreadLocal的任何引用,那么ThreadLocal就会被回收,此时其对应的value也就变得没有意义了,但是却无法被回收,这就造成了内存泄露。怎么解决?在ThreadLocal回收的时候记得调用其remove方法把entry移除,防止内存泄露。
ThreadLocal总结
ThreadLocal适合用于在不同线程作用域的数据备份
ThreadLocal机制通过在每个线程维护一个ThreadLocalMap,其key为ThreadLocal,value为ThreadLocal对应的泛型对象,这样每个ThreadLocal就可以作为key将不同的value存储在不同Thread的Map中,当获取数据的时候,同个ThreadLocal就可以在不同线程的Map中得到不同的数据,如下图:
ThreadLocalMap类似于一个改版的HashMap,内部也是使用数组和Hash算法来存储数据,使得存储和读取的速度非常快。
同时使用ThreadLocal需要注意内存泄露问题,当ThreadLocal不再使用的时候,需要通过remove方法把value移除。
最后
本文的重点是ThreadLocal。这是个很重要的类,是接下来要介绍的Looper关键类的线程存储方式。下一部分将开始介绍Handler机制的内部关键类。
希望文章对你有帮助。
“全文到此,原创不易,觉得有帮助可以点赞收藏评论转发。 笔者才疏学浅,有任何想法欢迎评论区交流指正。 如需转载请私信交流。
另外欢迎光临笔者的个人博客:传送门
”